196 lines
7.2 KiB
Markdown
196 lines
7.2 KiB
Markdown
# Database Documentation
|
|
|
|
MiauInv uses SQLite for persistent storage. The schema is initialized in `storage.InitDB` and foreign-key enforcement is explicitly enabled with:
|
|
|
|
```sql
|
|
PRAGMA foreign_keys = ON;
|
|
```
|
|
|
|
The database stores users, refresh tokens, 2FA recovery codes, inventory items, locations, projects, stock mappings, and project allocations.
|
|
|
|
## Entity Overview
|
|
|
|
```text
|
|
[users] 1 ──── N [refresh_tokens]
|
|
[users] 1 ──── N [two_factor_recovery_codes]
|
|
|
|
[items] 1 ──── N [stock] N ──── 1 [locations]
|
|
[items] 1 ──── N [project_items] N ──── 1 [projects]
|
|
```
|
|
|
|
## Tables
|
|
|
|
### `users`
|
|
|
|
Stores account credentials, roles, and 2FA state.
|
|
|
|
| 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. During setup the secret is held in a short-lived signed setup token and is only stored after the first valid TOTP code. |
|
|
|
|
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.
|
|
|
|
### `refresh_tokens`
|
|
|
|
Stores refresh-token sessions. Tokens are stored as hashes, not plaintext.
|
|
|
|
| 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. |
|
|
|
|
Refresh-token rotation revokes the used refresh token and inserts a new row for the next token.
|
|
|
|
### `two_factor_recovery_codes`
|
|
|
|
Stores recovery-code hashes for 2FA fallback login.
|
|
|
|
| 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. |
|
|
|
|
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.
|
|
|
|
### `items`
|
|
|
|
Stores tracked inventory items.
|
|
|
|
| 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. |
|
|
|
|
### `locations`
|
|
|
|
Stores physical or logical storage locations.
|
|
|
|
| Column | Type | Constraints | Description |
|
|
| --- | --- | --- | --- |
|
|
| `id` | `INTEGER` | Primary key, autoincrement | Location ID. |
|
|
| `name` | `TEXT` | Not null, unique | Location name. |
|
|
|
|
### `projects`
|
|
|
|
Stores project contexts for item allocation.
|
|
|
|
| Column | Type | Constraints | Description |
|
|
| --- | --- | --- | --- |
|
|
| `id` | `INTEGER` | Primary key, autoincrement | Project ID. |
|
|
| `name` | `TEXT` | Not null, unique | Project name. |
|
|
| `description` | `TEXT` | Optional | Project description. |
|
|
|
|
### `stock`
|
|
|
|
Maps item quantities to locations.
|
|
|
|
| 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 temporary setup token is validated.
|
|
2. The supplied TOTP code is validated against the setup secret.
|
|
3. Existing recovery codes are deleted.
|
|
4. New recovery-code hashes are inserted.
|
|
5. `users.two_factor_enabled` is set to `1` and `users.two_factor_secret` is set to the confirmed secret.
|
|
6. Existing refresh tokens are revoked and a new session is issued for the current browser.
|
|
|
|
### 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.
|