fix(docs): update NEXT_STEPS, README, and TEKNISK_BESKRIVNING with user role management details and new category structure

This commit is contained in:
Nils-Johan Gynther
2026-04-18 14:41:04 +02:00
parent c8489ed613
commit 00dc0d6c69
4 changed files with 98 additions and 30 deletions
+19 -10
View File
@@ -31,8 +31,8 @@
| Näringsvärden på produkter | ✅ Klart (schema + API) |
| Seed produktdata med kategoritilldelning | ⚠️ Script klart, ej aktiverat i init |
| Användarspecifika produkter (UserProduct) | ⚠️ Schema klart, UI basic |
| Användarroller (user / admin) | ❌ Saknas |
| Användarhantering i admin-UI | ❌ Saknas |
| Användarroller (user / admin) | ✅ Klart |
| Användarhantering i admin-UI | ✅ Klart |
| Teknisk skuld — oanvända InventoryItem-fält | ✅ Klart (migration 20260418) |
| Teknisk skuld — redirect-routes städade | ✅ Klart |
| Avancerad AI-integration (veckoplanering, kampanjdata) | ❌ Planerad |
@@ -51,15 +51,24 @@
- Generera en ny `002-seed-products.sql` med korrekt `categoryId` per rad (via `SELECT` mot live-db)
- Ta bort `.disabled`-suffixet och testa fresh install
### 2. Användarroller
**Mål:** Säkerställa att endast behöriga användare har admin-rättigheter.
### 2. Användarroller
**Klart.**
Idag har alla inloggade användare samma behörighetsnivå — ett säkerhetsproblem inför lansering.
- **Databas:** Lägg till `role` (enum `user` | `admin`) på `User`-modellen i Prisma + migration
- **Backend:** Rollbaserad guard (`@Roles('admin')`) — skyddar admin-endpoints; vanliga användare nekas med 403
- **Auth:** Rollen inkluderas i JWT-tokenen och i Auth.js session-objektet
- **Frontend — admin-UI (`/admin/users/`):** Lista användare, skapa nya konton (namn, e-post, lösenord, roll), ändra roll, avaktivera konto
- **Frontend — skyddade routes:** `/admin/*` kräver admin-roll; omdirigerar annars till startsidan
Systemet har nu fullständig rollbaserad åtkomstkontroll med två roller: `user` (standard) och `admin`.
**Vad som implementerades:**
- **Prisma-migration** (`20260418100000_add_user_role`) — fältet `role String @default("user")` lades till på `User`-modellen
- **`@Roles('admin')`-dekoratorn** (`auth/decorators/roles.decorator.ts`) — använder `SetMetadata` för att markera endpoints
- **`RolesGuard`** (`auth/roles.guard.ts`) — registrerad globalt som `APP_GUARD`; läser rollmetadata, kastar 403 om rätt roll saknas
- **JWT inkluderar nu `role`** — `jwt.strategy.ts` returnerar `{userId, username, role}`, `auth.service.ts` signerar med `role` i payload
- **Bootstrap-användare** (`users/admin-bootstrap.service.ts`) — `OnApplicationBootstrap` skapar/uppdaterar Nadmin, Padmin, user1 och user2 vid varje uppstart via miljövariabler
- **Skyddade produkt-endpoints** — `@Roles('admin')``merge`, `delete`, `restore`, `reset-all`, `bulk-update`, `backfill-canonical` i `products.controller.ts`
- **Användarhantering-API** — `GET /api/users` och `PATCH /api/users/:id/role` (båda kräver admin-roll)
- **Frontend-session** — `auth.ts` sparar `role` i JWT och session; `types/next-auth.d.ts` utgör typdefinition
- **Proxy-skydd** — `proxy.ts` blockerar `/admin/*` för användare utan admin-roll
- **Admin-UI** — `/admin/users` med tabell och rollbyteskäppar; länken "👥 Användare" i navigeringen visas enbart för admins
- **API-proxies** — `app/api/admin-users/route.ts` och `app/api/admin-users/[id]/route.ts` vidarebefordrar anrop med auth-header
### 3. Matplan-vy (frontend-polish) ✅
**Klart.**
+14
View File
@@ -52,6 +52,20 @@ En fullstack-applikation för hantering av hemmavaror och recept. Håll koll på
- **Ta bort och återställ** — soft-delete enskilda produkter, återställ med ett klick
- **Återställ all produktdata** — rensningsknapp som raderar alla produkter, inventarie, taggar och kvitto-alias (behåller användare och kategorier)
> Obs: Destruktiva åtgärder (merge, ta bort, återställ, bulk-uppdatera, återställ all data) kräver admin-roll.
### Admin: Användare
- **Lista alla användare** — se användarnamn, e-postadress, namn, roll och registreringsdatum
- **Ändra roll** — växla en användares roll mellan `user` och `admin` med ett klick
- **Skyddad sida** — `/admin/users` är enbart åtkomlig för inloggade användare med admin-roll; övriga omdirigeras till startsidan
- **Navigering** — länken "👥 Användare" visas bara i huvudmenyn om inloggad användare har admin-roll
### Autentisering och roller
- **Rollbaserad åtkomstkontroll** — systemet har två roller: `user` (standard) och `admin`
- **Automatisk bootstrap** — fyra användare skapas eller uppdateras automatiskt när backend startar, baserat på miljövariabler:
- `Nadmin` (admin), `Padmin` (admin), `user1` (user), `user2` (user)
- **Skyddade admin-endpoints** — destruktiva produkt-endpoints och användarhantering kräver `admin`-roll; försök utan rätt roll ger 403 Förbjuden
### Användarprofil
- **Redigera profilinformation** — uppdatera förnamn, efternamn och e-postadress under "Min profil"
+39 -20
View File
@@ -89,6 +89,8 @@ docker exec recipe-db mariadb -uroot -p"LÖSENORD" recipe_app -e "SHOW TABLES;"
| | `app/matplan/MealPlanClient.tsx` | Veckovy, receptval per dag, portionsjustering, inköpslista, inventariejämförelse |
| **Kvittoimport** | `app/import/page.tsx` | Server component med Navigation + flikvy |
| | `app/import/ImportTabsClient.tsx` | Klientkomponent: kvitto/recept-flikar |
| **Admin: Användare** | `app/admin/users/page.tsx` | Server component: hämtar alla användare, kräver admin-roll |
| | `app/admin/users/UserAdminClient.tsx` | Klientkomponent: tabell med rollbyte-knappar, anropar `/api/admin-users/:id` |
| **Admin: Produkter** | `app/admin/products/page.tsx` | Produktadmin-panel |
| | `AdminProductList.tsx` | Lista produkter, sök, sortera, filter okategoriserade, bulk-select + bulk-kategorisering |
| | `EditProductForm.tsx` | Inline redigering: name, canonicalName, kategori (hierarkisk dropdown), brand, taggar |
@@ -106,6 +108,8 @@ Alla proxy-routes läser auth-token via `auth()` (Auth.js v5) och vidarebefordra
| Route | Metod | Syfte |
|-------|-------|-------|
| `/api/admin-users` | GET | Hämtar alla användare (kräver admin-roll i session) |
| `/api/admin-users/[id]` | PATCH | Ändrar roll för användare med givet id (kräver admin-roll i session) |
| `/api/auth/[...nextauth]` | GET, POST | Auth.js handlers (login, logout, session) |
| `/api/products` | GET | Produktlista (auth-wrappat med `auth(req)`) |
| `/api/categories` | GET | Kategorihierarki (publik, proxies `/api/categories/tree`) |
@@ -124,8 +128,8 @@ Alla proxy-routes läser auth-token via `auth()` (Auth.js v5) och vidarebefordra
### Autentisering (Auth.js v5)
- `auth.ts` — NextAuth-konfiguration med Credentials provider
- `middleware.ts` — Skyddar alla routes utom `/login`, `/register` och `/api/auth`
- `auth.ts` — NextAuth-konfiguration med Credentials provider; sparar `accessToken`, `userId`, `username` och `role` i JWT-token och session
- `proxy.ts` — Skyddar alla routes utom `/login`, `/register` och `/api/auth`; blockerar `/admin/*` om sessionens `role` inte är `admin`
- `lib/auth-headers.ts``getAuthHeaders()` hämtar Bearer-token från session (server-side)
- `lib/api.ts``fetchJson()` lägger automatiskt till auth-headers server-side, redirectar till `/login` vid 401
@@ -143,7 +147,8 @@ Alla proxy-routes läser auth-token via `auth()` (Auth.js v5) och vidarebefordra
- **Språk:** TypeScript 5.4.5
- **Databas:** MariaDB 11 (via Prisma 6.12.0 ORM)
- **API:** REST, validering med class-validator
- **Autentisering:** JWT (7 dagars token), JwtAuthGuard skyddar alla routes, `@Public()` dekorator för öppna endpoints
- **Autentisering:** JWT (7 dagars token), `JwtAuthGuard` skyddar alla routes globalt, `RolesGuard` kontrollerar rollkrav, `@Public()` dekorator för öppna endpoints
- **Rollbaserad behörighet:** `@Roles('admin')` dekoratorn via `SetMetadata`; `RolesGuard` kastar 403 om rätt roll saknas
- **Felhantering:** GlobalExceptionFilter (svenska felmeddelanden)
- **Hälsokontroll:** /health endpoints
- **Bygg:** `nest build`, körs i Docker-container
@@ -155,16 +160,22 @@ backend/src/
├── app.module.ts # Root module
├── main.ts # Startpunkt (port 8080, global prefix "api")
├── auth/
│ ├── auth.controller.ts # POST /api/auth/login
│ ├── auth.service.ts # validateUser, login (JWT-signering)
│ ├── auth.controller.ts # POST /api/auth/login, POST /api/auth/register
│ ├── auth.service.ts # validateUser, login (JWT-signering inkl. role)
│ ├── auth.module.ts
│ ├── jwt.strategy.ts # Passport JWT-strategi
│ ├── jwt.strategy.ts # Passport JWT-strategi (returnerar userId, username, role)
│ ├── jwt-auth.guard.ts # Global guard (skyddar allt utom @Public)
│ ├── roles.guard.ts # Guard som kontrollerar @Roles() metadata; kastar 403
│ └── decorators/
── public.decorator.ts # @Public() markerar öppen endpoint
── public.decorator.ts # @Public() markerar öppen endpoint
│ ├── current-user.decorator.ts # @CurrentUser() extraherar {userId, username, role}
│ └── roles.decorator.ts # @Roles('admin') sätter rollkrav via SetMetadata
├── users/
│ ├── users.controller.ts # GET/PATCH /api/users/me
├── users.service.ts # findByUsername, create, updateProfile
│ ├── users.controller.ts # GET /api/users/me, PATCH /api/users/me
│ # GET /api/users (admin), PATCH /api/users/:id/role (admin)
│ ├── users.service.ts # findByUsername, findById, create, updateProfile
│ │ # findAll, setRole
│ ├── admin-bootstrap.service.ts # OnApplicationBootstrap: skapar/uppdaterar 4 seed-användare
│ └── users.module.ts
├── categories/
│ ├── categories.controller.ts # GET /api/categories, GET /api/categories/tree (@Public)
@@ -379,9 +390,11 @@ GET /api/categories/tree Hierarkiskt träd (@Public)
### Användar-endpoints
```
POST /api/auth/login Logga in, returnerar JWT (@Public)
GET /api/users/me Hämta inloggad användares profil
POST /api/auth/login Logga in, returnerar JWT inkl. role (@Public)
GET /api/users/me Hämta inloggad användares profil (inkl. role)
PATCH /api/users/me Uppdatera firstName, lastName, email
GET /api/users Lista alla användare (kräver admin-roll)
PATCH /api/users/:id/role Ändra roll för användare (kräver admin-roll)
```
### Baslager-endpoints
@@ -422,17 +435,29 @@ DELETE /api/receipt-alias/:id Ta bort alias
### User
```prisma
model User {
id Int @id @default(autoincrement())
username String @unique
email String @unique
id Int @id @default(autoincrement())
username String @unique
email String @unique
firstName String?
lastName String?
passwordHash String
role String @default("user") # "user" eller "admin"
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
```
**Bootstrap-användare:** När backend startar kör `AdminBootstrapService.onApplicationBootstrap()` och skapar eller uppdaterar fyra användare baserade på miljövariabler:
| Användarnamn | Roll | E-post | Miljövariabel |
|---|---|---|---|
| Nadmin | admin | nadmin@localhost | `ADMIN_NADMIN_PASSWORD` |
| Padmin | admin | padmin@localhost | `ADMIN_PADMIN_PASSWORD` |
| user1 | user | user1@localhost | `SEED_USER1_PASSWORD` |
| user2 | user | user2@localhost | `SEED_USER2_PASSWORD` |
Om en användare redan finns men har fel roll rättas rollen automatiskt. Om miljövariabeln saknas hoppas den användaren över med en varning i loggen.
### Category
```prisma
model Category {
@@ -495,16 +520,10 @@ model InventoryItem {
unit String # Enhet (g, kg, ml, dl, st, tsk, msk, etc)
brand String? # Varumärke
location String? # Lagerplats (Kyl, Frys, Skafferi, etc)
priority Int? # Prioritetsordning
purchaseDate DateTime? # Köpdatum
opened Boolean? # Markering för öppnad produkt
shelfNote String? # Lagringsnot
suitableFor String? # Lämplighetsmärkning (t.ex. "vegetarian")
isOnSale Boolean? # Är på rea
priceLevel Int? # Priskategori (15)
bestBeforeDate DateTime? # Bäst före-datum
proteinType String? # Proteintyp (t.ex. "beef", "chicken")
isLeftover Boolean? # Är från tidigare lagnning
comment String? # Fri kommentar
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
+26
View File
@@ -283,3 +283,29 @@ INSERT IGNORE INTO `Category` (`name`, `parentId`)
FROM `Category` c1
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Asien'
WHERE c1.name = 'Skafferi' AND c1.parentId IS NULL;
-- ============================================================
-- LEVEL 1 — Övrigt (ny toppkategori)
-- ============================================================
INSERT IGNORE INTO `Category` (`name`, `parentId`) VALUES
('Övrigt', NULL);
-- ============================================================
-- LEVEL 2 — under Övrigt
-- ============================================================
INSERT IGNORE INTO `Category` (`name`, `parentId`)
SELECT 'Frukt och grönt', id FROM `Category` WHERE name = 'Övrigt' AND parentId IS NULL;
INSERT IGNORE INTO `Category` (`name`, `parentId`)
SELECT 'Kött', id FROM `Category` WHERE name = 'Övrigt' AND parentId IS NULL;
INSERT IGNORE INTO `Category` (`name`, `parentId`)
SELECT 'Fisk', id FROM `Category` WHERE name = 'Övrigt' AND parentId IS NULL;
INSERT IGNORE INTO `Category` (`name`, `parentId`)
SELECT 'Mejeri', id FROM `Category` WHERE name = 'Övrigt' AND parentId IS NULL;
INSERT IGNORE INTO `Category` (`name`, `parentId`)
SELECT 'Fryst', id FROM `Category` WHERE name = 'Övrigt' AND parentId IS NULL;
INSERT IGNORE INTO `Category` (`name`, `parentId`)
SELECT 'Skafferi', id FROM `Category` WHERE name = 'Övrigt' AND parentId IS NULL;
INSERT IGNORE INTO `Category` (`name`, `parentId`)
SELECT 'Barn och Godis', id FROM `Category` WHERE name = 'Övrigt' AND parentId IS NULL;
INSERT IGNORE INTO `Category` (`name`, `parentId`)
SELECT 'Dryck', id FROM `Category` WHERE name = 'Övrigt' AND parentId IS NULL;