updated docs for new feature
This commit is contained in:
219
docs/DATABASE.md
219
docs/DATABASE.md
@@ -1,97 +1,194 @@
|
||||
# Database Documentation
|
||||
|
||||
MiauInv utilizes an embedded SQLite database instance for persistent data storage. Foreign key constraints are strictly enforced at the database level.
|
||||
MiauInv uses SQLite for persistent storage. The schema is initialized in `storage.InitDB` and foreign-key enforcement is explicitly enabled with:
|
||||
|
||||
## Configuration
|
||||
To ensure data integrity, every database connection initialization explicitly executes the following command before handling queries:
|
||||
```sql
|
||||
PRAGMA foreign_keys = ON;
|
||||
```
|
||||
|
||||
---
|
||||
The database stores users, refresh tokens, 2FA recovery codes, inventory items, locations, projects, stock mappings, and project allocations.
|
||||
|
||||
## Schema Architecture
|
||||
## Entity Overview
|
||||
|
||||
### Entity-Relationship Summary
|
||||
```text
|
||||
[users] 1 ──── N [refresh_tokens]
|
||||
[users] 1 ──── N [two_factor_recovery_codes]
|
||||
|
||||
The database consists of primary entity tables (`users`, `items`, `locations`, `projects`) and relational junction tables (`stock`, `project_items`, `refresh_tokens`) designed to track stock distribution and access sessions.
|
||||
|
||||
```
|
||||
[users] <--- (1:N) ---> [refresh_tokens]
|
||||
[items] <--- (1:N) ---> [stock] <--- (N:1) ---> [locations]
|
||||
[items] <--- (1:N) ---> [project_items] <--- (N:1) ---> [projects]
|
||||
[items] 1 ──── N [stock] N ──── 1 [locations]
|
||||
[items] 1 ──── N [project_items] N ──── 1 [projects]
|
||||
```
|
||||
|
||||
---
|
||||
## Tables
|
||||
|
||||
## Table Definitions
|
||||
### `users`
|
||||
|
||||
### 1. users
|
||||
Stores account credentials, roles, and 2FA state.
|
||||
|
||||
Stores user credentials and operational roles within the system.
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `TEXT` | Primary key | User UUID. |
|
||||
| `username` | `TEXT` | Not null, unique | Lowercased account name. |
|
||||
| `password` | `TEXT` | Not null | bcrypt password hash. |
|
||||
| `role` | `TEXT` | Not null | User role, for example `user` or `admin`. |
|
||||
| `two_factor_enabled` | `INTEGER` | Not null, default `0` | Boolean flag for TOTP 2FA state. |
|
||||
| `two_factor_secret` | `TEXT` | Not null, default `''` | TOTP secret used to validate authenticator codes. Empty when 2FA is disabled. |
|
||||
|
||||
* **id (TEXT, PK):** Unique UUID
|
||||
* **username (TEXT, Unique):** Unique account identifier.
|
||||
* **password (TEXT):** Hashed user password.
|
||||
* **role (TEXT):** Access control flag (e.g., admin, user).
|
||||
Migration note: existing databases are migrated with `ALTER TABLE` statements for `two_factor_enabled` and `two_factor_secret` if those columns do not exist yet.
|
||||
|
||||
### 2. refresh_tokens
|
||||
### `refresh_tokens`
|
||||
|
||||
Tracks valid extended sessions linked to specific user accounts.
|
||||
Stores refresh-token sessions. Tokens are stored as hashes, not plaintext.
|
||||
|
||||
* **id (TEXT, PK):** Unique identifier.
|
||||
* **user_id (TEXT, FK):** References `users(id)`.
|
||||
* **token_hash (TEXT):** Cryptographic hash of the active refresh token.
|
||||
* **expires_at (INTEGER):** Unix timestamp indicating token expiration.
|
||||
* **created_at (INTEGER):** Unix timestamp indicating session creation.
|
||||
* **revoked (INTEGER):** Boolean flag (0 or 1) indicating if the session was manually invalidated.
|
||||
* **device_info (TEXT, Optional):** Client metadata for auditing.
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `TEXT` | Primary key | Refresh-token row UUID. |
|
||||
| `user_id` | `TEXT` | Not null, foreign key to `users(id)` | Owning user. |
|
||||
| `token_hash` | `TEXT` | Not null | Hash of the refresh token. |
|
||||
| `expires_at` | `INTEGER` | Not null | Unix timestamp for expiry. |
|
||||
| `created_at` | `INTEGER` | Not null | Unix timestamp for creation. |
|
||||
| `revoked` | `INTEGER` | Not null, default `0` | Boolean revocation flag. |
|
||||
| `device_info` | `TEXT` | Optional | User-Agent string recorded when the session is created. |
|
||||
|
||||
### 3. items
|
||||
Refresh-token rotation revokes the used refresh token and inserts a new row for the next token.
|
||||
|
||||
Represents individual tracked assets.
|
||||
### `two_factor_recovery_codes`
|
||||
|
||||
* **id (INTEGER, PK, Autoincrement):** Primary key.
|
||||
* **name (TEXT):** Asset designation.
|
||||
* **category (TEXT, Optional):** Grouping classification.
|
||||
* **description (TEXT, Optional):** Detailed asset context.
|
||||
* **total_quantity (INTEGER):** Absolute global stock baseline counter.
|
||||
Stores recovery-code hashes for 2FA fallback login.
|
||||
|
||||
### 4. locations
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `TEXT` | Primary key | Recovery-code row UUID. |
|
||||
| `user_id` | `TEXT` | Not null, foreign key to `users(id)` with `ON DELETE CASCADE` | Owning user. |
|
||||
| `code_hash` | `TEXT` | Not null, unique with `user_id` | Hash of the normalized recovery code. |
|
||||
| `created_at` | `INTEGER` | Not null | Unix timestamp for creation. |
|
||||
| `used_at` | `INTEGER` | Nullable | Unix timestamp when the code was consumed. `NULL` means unused. |
|
||||
|
||||
Defines logical or physical facilities.
|
||||
Recovery codes are deleted and replaced when the user regenerates them. A recovery code is consumed with an atomic update that only matches unused codes.
|
||||
|
||||
* **id (INTEGER, PK, Autoincrement):** Primary key.
|
||||
* **name (TEXT, Unique):** Unique facility naming constraint.
|
||||
### `items`
|
||||
|
||||
### 5. projects
|
||||
Stores tracked inventory items.
|
||||
|
||||
Defines distinct tasks or allocation targets.
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `INTEGER` | Primary key, autoincrement | Item ID. |
|
||||
| `name` | `TEXT` | Not null | Item name. |
|
||||
| `category` | `TEXT` | Optional | Category label. |
|
||||
| `description` | `TEXT` | Optional | Item description. |
|
||||
| `total_quantity` | `INTEGER` | Not null, default `0` | Global quantity baseline. |
|
||||
|
||||
* **id (INTEGER, PK, Autoincrement):** Primary key.
|
||||
* **name (TEXT, Unique):** Unique operational tracking name.
|
||||
* **description (TEXT, Optional):** Scope description.
|
||||
### `locations`
|
||||
|
||||
### 6. stock
|
||||
Stores physical or logical storage locations.
|
||||
|
||||
Junction table mapping physical asset distributions across facilities.
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `INTEGER` | Primary key, autoincrement | Location ID. |
|
||||
| `name` | `TEXT` | Not null, unique | Location name. |
|
||||
|
||||
* **id (INTEGER, PK, Autoincrement):** Primary key.
|
||||
* **item_id (INTEGER, FK):** References `items(id)`.
|
||||
* **location_id (INTEGER, FK):** References `locations(id)`.
|
||||
* **quantity (INTEGER):** Specific quantity present at this location node.
|
||||
### `projects`
|
||||
|
||||
### 7. project_items
|
||||
Stores project contexts for item allocation.
|
||||
|
||||
Junction table tracking asset assignments dedicated to specific ongoing project environments.
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `INTEGER` | Primary key, autoincrement | Project ID. |
|
||||
| `name` | `TEXT` | Not null, unique | Project name. |
|
||||
| `description` | `TEXT` | Optional | Project description. |
|
||||
|
||||
* **id (INTEGER, PK, Autoincrement):** Primary key.
|
||||
* **item_id (INTEGER, FK):** References `items(id)`.
|
||||
* **project_id (INTEGER, FK):** References `projects(id)`.
|
||||
* **quantity (INTEGER):** Quantity allocated to this project context.
|
||||
---
|
||||
### `stock`
|
||||
|
||||
## Data Integrity Constraints
|
||||
Maps item quantities to locations.
|
||||
|
||||
* **Foreign Keys:** Because standard `ON DELETE` cascades are not defined explicitly in the schema rules, SQLite blocks parent deletion actions if dependent rows exist in `stock` or `project_items`. You must clear out stock allocations and project associations manually before deleting an item, location, or project.
|
||||
* **Uniqueness:** String uniqueness constraints protect against duplicate namespace registration on `users(username)`, `locations(name)`, and `projects(name)`.
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `INTEGER` | Primary key, autoincrement | Stock row ID. |
|
||||
| `item_id` | `INTEGER` | Not null, foreign key to `items(id)` | Item reference. |
|
||||
| `location_id` | `INTEGER` | Not null, foreign key to `locations(id)` | Location reference. |
|
||||
| `quantity` | `INTEGER` | Not null | Quantity at this location. |
|
||||
|
||||
### `project_items`
|
||||
|
||||
Maps item quantities to projects.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `id` | `INTEGER` | Primary key, autoincrement | Association row ID. |
|
||||
| `item_id` | `INTEGER` | Not null, foreign key to `items(id)` | Item reference. |
|
||||
| `project_id` | `INTEGER` | Not null, foreign key to `projects(id)` | Project reference. |
|
||||
| `quantity` | `INTEGER` | Not null | Quantity allocated to the project. |
|
||||
|
||||
## Data Integrity
|
||||
|
||||
### Foreign keys
|
||||
|
||||
Foreign keys are enabled per connection. Because most inventory foreign keys do not define explicit cascade behavior, SQLite blocks deletion of referenced items, locations, or projects while dependent rows exist.
|
||||
|
||||
`two_factor_recovery_codes.user_id` uses `ON DELETE CASCADE`, so deleting a user also deletes their recovery-code rows.
|
||||
|
||||
### Uniqueness
|
||||
|
||||
The following values are unique:
|
||||
|
||||
- `users.username`
|
||||
- `locations.name`
|
||||
- `projects.name`
|
||||
- `two_factor_recovery_codes(user_id, code_hash)`
|
||||
|
||||
### Current schema limitations
|
||||
|
||||
The current schema does not yet enforce all business rules at the database level. In particular:
|
||||
|
||||
- `items.total_quantity` has no explicit `CHECK(total_quantity >= 0)` constraint.
|
||||
- `stock.quantity` has no explicit `CHECK(quantity >= 0)` constraint.
|
||||
- `project_items.quantity` has no explicit `CHECK(quantity >= 0)` constraint.
|
||||
- Duplicate stock rows for the same `(item_id, location_id)` pair are not prevented by a unique constraint.
|
||||
- Duplicate project allocations for the same `(item_id, project_id)` pair are not prevented by a unique constraint.
|
||||
|
||||
These constraints should be added in a future migration once the desired application behavior is finalized.
|
||||
|
||||
## Important Queries and Behaviors
|
||||
|
||||
### User lookup
|
||||
|
||||
Users can be loaded by lowercased username or ID. Username updates store the new username lowercased.
|
||||
|
||||
### Password update
|
||||
|
||||
When the password is updated:
|
||||
|
||||
1. The new password is bcrypt-hashed.
|
||||
2. The `users.password` field is updated.
|
||||
3. Existing refresh tokens for that user are revoked.
|
||||
4. A new session is issued.
|
||||
|
||||
### 2FA enable
|
||||
|
||||
When 2FA is enabled:
|
||||
|
||||
1. The TOTP secret must already exist from `/api/2fa/setup`.
|
||||
2. The supplied TOTP code is validated.
|
||||
3. Existing recovery codes are deleted.
|
||||
4. New recovery-code hashes are inserted.
|
||||
5. `users.two_factor_enabled` is set to `1`.
|
||||
|
||||
### 2FA disable
|
||||
|
||||
When 2FA is disabled:
|
||||
|
||||
1. `two_factor_enabled` is set to `0`.
|
||||
2. `two_factor_secret` is cleared.
|
||||
3. Recovery-code rows for the user are deleted.
|
||||
4. Refresh tokens for the user are revoked.
|
||||
|
||||
### Recovery-code use
|
||||
|
||||
A recovery code is consumed with an update equivalent to:
|
||||
|
||||
```sql
|
||||
UPDATE two_factor_recovery_codes
|
||||
SET used_at = ?
|
||||
WHERE user_id = ? AND code_hash = ? AND used_at IS NULL;
|
||||
```
|
||||
|
||||
The login succeeds only if exactly one row is updated.
|
||||
|
||||
Reference in New Issue
Block a user