Compare commits
2 Commits
9ee061d5f3
...
4f387fe6eb
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f387fe6eb | |||
| 8bc1bd3e21 |
@@ -1032,6 +1032,19 @@ Kvitto-alias lagrar mappningar från kvittots råtext till produkt-ID. Nä
|
|||||||
|
|
||||||
## Arkitektur: User-scope för baslager och matplan (2026-04-22)
|
## Arkitektur: User-scope för baslager och matplan (2026-04-22)
|
||||||
|
|
||||||
|
> **Designregel: User-scope vid ny funktionalitet**
|
||||||
|
>
|
||||||
|
> All data som tillhör en specifik användare **måste** ha ett `userId`-fält med FK till `User` och user-filtrering i service-lagret. Följ detta mönster vid all ny funktionsutveckling:
|
||||||
|
>
|
||||||
|
> 1. **Databasschema:** `userId Int` med `@relation` till `User`, icke-null. Sammansatt `@@unique([userId, ...])` om raden ska vara unik per användare.
|
||||||
|
> 2. **Migration:** Backfill till första användaren för befintliga rader innan kolumnen sätts NOT NULL.
|
||||||
|
> 3. **Backend service:** Alla `findAll`/`findOne`/`create`/`delete` filtrerar eller sätter `where: { userId }`. Returnera aldrig andra användares data.
|
||||||
|
> 4. **Controller:** Extrahera `userId` via `@CurrentUser()` eller `@Request() req` — aldrig från body (säkerhetsrisk).
|
||||||
|
> 5. **Globala vs. privata resurser:** Produkter och kategorier är globala. Inventarie, baslager, matplan och användarspecifika produkter (`isPrivate: true`) är user-scopade.
|
||||||
|
> 6. **Privata produkter** (`isPrivate: true`): skapas via `POST /products/private`, visas via `GET /products/mine`. Normaliserat namn prefixas med `private:{userId}:` för att undvika kollision med globala produkter.
|
||||||
|
>
|
||||||
|
> **Kontrollera alltid:** Kan två användare se varandras data? Om ja — lägg till `userId`-scope.
|
||||||
|
|
||||||
### Bakgrund och beslut
|
### Bakgrund och beslut
|
||||||
|
|
||||||
Baslager (`PantryItem`) och matplan (`MealPlanEntry`) var ursprungligen globala — delade av alla användare. Det skapade problem när flera användare loggade in eftersom de såg och påverkade varandras data.
|
Baslager (`PantryItem`) och matplan (`MealPlanEntry`) var ursprungligen globala — delade av alla användare. Det skapade problem när flera användare loggade in eftersom de såg och påverkade varandras data.
|
||||||
|
|||||||
@@ -49,18 +49,28 @@ class CategoryThenProductPicker {
|
|||||||
int? preselectedCategoryId,
|
int? preselectedCategoryId,
|
||||||
Future<ProductOption?> Function(String name, int categoryId)? onCreate,
|
Future<ProductOption?> Function(String name, int categoryId)? onCreate,
|
||||||
}) async {
|
}) async {
|
||||||
// Steg 1 — välj kategori
|
AdminCategoryNode? selectedCategory;
|
||||||
final selectedCategory = await showModalBottomSheet<AdminCategoryNode>(
|
|
||||||
context: context,
|
if (preselectedCategoryId != null) {
|
||||||
isScrollControlled: true,
|
// AI-föreslagen kategori — hoppa direkt till produktpickern
|
||||||
useSafeArea: true,
|
selectedCategory = _findNode(categoryTree, preselectedCategoryId);
|
||||||
builder: (ctx) => _CategoryPickerSheet(
|
}
|
||||||
tree: categoryTree,
|
|
||||||
preselectedId: preselectedCategoryId,
|
if (selectedCategory == null) {
|
||||||
onSelected: (node) => Navigator.pop(ctx, node),
|
// Steg 1 — visa kategoriträd
|
||||||
),
|
if (!context.mounted) return null;
|
||||||
);
|
selectedCategory = await showModalBottomSheet<AdminCategoryNode>(
|
||||||
if (selectedCategory == null || !context.mounted) return null;
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
useSafeArea: true,
|
||||||
|
builder: (ctx) => _CategoryPickerSheet(
|
||||||
|
tree: categoryTree,
|
||||||
|
preselectedId: preselectedCategoryId,
|
||||||
|
onSelected: (node) => Navigator.pop(ctx, node),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (selectedCategory == null || !context.mounted) return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Samla alla kategori-IDs i den valda grenen (inkl. ättlingar)
|
// Samla alla kategori-IDs i den valda grenen (inkl. ättlingar)
|
||||||
final categoryIds = _collectIds(selectedCategory);
|
final categoryIds = _collectIds(selectedCategory);
|
||||||
@@ -319,6 +329,16 @@ class _CategoryTileState extends State<_CategoryTile> {
|
|||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
trailing: TextButton(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
minimumSize: const Size(0, 32),
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
onPressed: () => widget.onLeafTap(node),
|
||||||
|
child: const Text('Välj'),
|
||||||
|
),
|
||||||
onTap: () => setState(() => _expanded = !_expanded),
|
onTap: () => setState(() => _expanded = !_expanded),
|
||||||
),
|
),
|
||||||
if (_expanded)
|
if (_expanded)
|
||||||
|
|||||||
@@ -58,6 +58,23 @@ Viktigt att komma ihåg vid implementering av nya funktioner och kodning är att
|
|||||||
|
|
||||||
## Kända fallgropar och API-gotchas
|
## Kända fallgropar och API-gotchas
|
||||||
|
|
||||||
|
### Designregel: User-scope vid ny funktionalitet
|
||||||
|
|
||||||
|
> **Regel:** Kontrollera alltid om ny data tillhör en specifik användare. Om ja — data **måste** ha user-scope i backend (`userId`-fält, filtrering i service) och Flutter-klienten får **inte** returnera/visa andra användares data.
|
||||||
|
>
|
||||||
|
> Resurser och deras scope:
|
||||||
|
> | Resurs | Scope |
|
||||||
|
> |---|---|
|
||||||
|
> | Produkter (globala) | Publik — `GET /products` |
|
||||||
|
> | Produkter (privata) | Per användare — `GET /products/mine`, `POST /products/private` |
|
||||||
|
> | Kategorier | Global (admin-only att skapa) |
|
||||||
|
> | Inventarie | Per användare — alltid filtrera på `userId` |
|
||||||
|
> | Baslager | Per användare — alltid filtrera på `userId` |
|
||||||
|
> | Matplan | Per användare — alltid filtrera på `userId` |
|
||||||
|
> | Recept | Per användare (ägare) eller delat |
|
||||||
|
>
|
||||||
|
> I Flutter: ladda user-scopad data med token, kombinera globala och egna produktlistor vid behov.
|
||||||
|
|
||||||
### Flutter Web och PDF MIME-typ
|
### Flutter Web och PDF MIME-typ
|
||||||
- Flutter Web skickar PDF-filer med MIME-typ `application/octet-stream` istället för `application/pdf`.
|
- Flutter Web skickar PDF-filer med MIME-typ `application/octet-stream` istället för `application/pdf`.
|
||||||
- Backend (`receipt-import.controller.ts`) måste tillåta båda: `application/pdf` och `application/octet-stream` i `ALLOWED_MIMES`.
|
- Backend (`receipt-import.controller.ts`) måste tillåta båda: `application/pdf` och `application/octet-stream` i `ALLOWED_MIMES`.
|
||||||
|
|||||||
Reference in New Issue
Block a user