fix(docs): update NEXT_STEPS, README, and TEKNISK_BESKRIVNING with user role management details and new category structure
This commit is contained in:
+19
-10
@@ -31,8 +31,8 @@
|
|||||||
| Näringsvärden på produkter | ✅ Klart (schema + API) |
|
| Näringsvärden på produkter | ✅ Klart (schema + API) |
|
||||||
| Seed produktdata med kategoritilldelning | ⚠️ Script klart, ej aktiverat i init |
|
| Seed produktdata med kategoritilldelning | ⚠️ Script klart, ej aktiverat i init |
|
||||||
| Användarspecifika produkter (UserProduct) | ⚠️ Schema klart, UI basic |
|
| Användarspecifika produkter (UserProduct) | ⚠️ Schema klart, UI basic |
|
||||||
| Användarroller (user / admin) | ❌ Saknas |
|
| Användarroller (user / admin) | ✅ Klart |
|
||||||
| Användarhantering i admin-UI | ❌ Saknas |
|
| Användarhantering i admin-UI | ✅ Klart |
|
||||||
| Teknisk skuld — oanvända InventoryItem-fält | ✅ Klart (migration 20260418) |
|
| Teknisk skuld — oanvända InventoryItem-fält | ✅ Klart (migration 20260418) |
|
||||||
| Teknisk skuld — redirect-routes städade | ✅ Klart |
|
| Teknisk skuld — redirect-routes städade | ✅ Klart |
|
||||||
| Avancerad AI-integration (veckoplanering, kampanjdata) | ❌ Planerad |
|
| 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)
|
- 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
|
- Ta bort `.disabled`-suffixet och testa fresh install
|
||||||
|
|
||||||
### 2. Användarroller
|
### 2. Användarroller ✅
|
||||||
**Mål:** Säkerställa att endast behöriga användare har admin-rättigheter.
|
**Klart.**
|
||||||
|
|
||||||
Idag har alla inloggade användare samma behörighetsnivå — ett säkerhetsproblem inför lansering.
|
Systemet har nu fullständig rollbaserad åtkomstkontroll med två roller: `user` (standard) och `admin`.
|
||||||
- **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
|
**Vad som implementerades:**
|
||||||
- **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
|
- **Prisma-migration** (`20260418100000_add_user_role`) — fältet `role String @default("user")` lades till på `User`-modellen
|
||||||
- **Frontend — skyddade routes:** `/admin/*` kräver admin-roll; omdirigerar annars till startsidan
|
- **`@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')` på `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) ✅
|
### 3. Matplan-vy (frontend-polish) ✅
|
||||||
**Klart.**
|
**Klart.**
|
||||||
|
|||||||
@@ -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
|
- **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)
|
- **Å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
|
### Användarprofil
|
||||||
- **Redigera profilinformation** — uppdatera förnamn, efternamn och e-postadress under "Min profil"
|
- **Redigera profilinformation** — uppdatera förnamn, efternamn och e-postadress under "Min profil"
|
||||||
|
|
||||||
|
|||||||
+36
-17
@@ -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 |
|
| | `app/matplan/MealPlanClient.tsx` | Veckovy, receptval per dag, portionsjustering, inköpslista, inventariejämförelse |
|
||||||
| **Kvittoimport** | `app/import/page.tsx` | Server component med Navigation + flikvy |
|
| **Kvittoimport** | `app/import/page.tsx` | Server component med Navigation + flikvy |
|
||||||
| | `app/import/ImportTabsClient.tsx` | Klientkomponent: kvitto/recept-flikar |
|
| | `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 |
|
| **Admin: Produkter** | `app/admin/products/page.tsx` | Produktadmin-panel |
|
||||||
| | `AdminProductList.tsx` | Lista produkter, sök, sortera, filter okategoriserade, bulk-select + bulk-kategorisering |
|
| | `AdminProductList.tsx` | Lista produkter, sök, sortera, filter okategoriserade, bulk-select + bulk-kategorisering |
|
||||||
| | `EditProductForm.tsx` | Inline redigering: name, canonicalName, kategori (hierarkisk dropdown), brand, taggar |
|
| | `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 |
|
| 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/auth/[...nextauth]` | GET, POST | Auth.js handlers (login, logout, session) |
|
||||||
| `/api/products` | GET | Produktlista (auth-wrappat med `auth(req)`) |
|
| `/api/products` | GET | Produktlista (auth-wrappat med `auth(req)`) |
|
||||||
| `/api/categories` | GET | Kategorihierarki (publik, proxies `/api/categories/tree`) |
|
| `/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)
|
### Autentisering (Auth.js v5)
|
||||||
|
|
||||||
- `auth.ts` — NextAuth-konfiguration med Credentials provider
|
- `auth.ts` — NextAuth-konfiguration med Credentials provider; sparar `accessToken`, `userId`, `username` och `role` i JWT-token och session
|
||||||
- `middleware.ts` — Skyddar alla routes utom `/login`, `/register` och `/api/auth`
|
- `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/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
|
- `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
|
- **Språk:** TypeScript 5.4.5
|
||||||
- **Databas:** MariaDB 11 (via Prisma 6.12.0 ORM)
|
- **Databas:** MariaDB 11 (via Prisma 6.12.0 ORM)
|
||||||
- **API:** REST, validering med class-validator
|
- **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)
|
- **Felhantering:** GlobalExceptionFilter (svenska felmeddelanden)
|
||||||
- **Hälsokontroll:** /health endpoints
|
- **Hälsokontroll:** /health endpoints
|
||||||
- **Bygg:** `nest build`, körs i Docker-container
|
- **Bygg:** `nest build`, körs i Docker-container
|
||||||
@@ -155,16 +160,22 @@ backend/src/
|
|||||||
├── app.module.ts # Root module
|
├── app.module.ts # Root module
|
||||||
├── main.ts # Startpunkt (port 8080, global prefix "api")
|
├── main.ts # Startpunkt (port 8080, global prefix "api")
|
||||||
├── auth/
|
├── auth/
|
||||||
│ ├── auth.controller.ts # POST /api/auth/login
|
│ ├── auth.controller.ts # POST /api/auth/login, POST /api/auth/register
|
||||||
│ ├── auth.service.ts # validateUser, login (JWT-signering)
|
│ ├── auth.service.ts # validateUser, login (JWT-signering inkl. role)
|
||||||
│ ├── auth.module.ts
|
│ ├── 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)
|
│ ├── jwt-auth.guard.ts # Global guard (skyddar allt utom @Public)
|
||||||
|
│ ├── roles.guard.ts # Guard som kontrollerar @Roles() metadata; kastar 403
|
||||||
│ └── decorators/
|
│ └── 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/
|
||||||
│ ├── users.controller.ts # GET/PATCH /api/users/me
|
│ ├── users.controller.ts # GET /api/users/me, PATCH /api/users/me
|
||||||
│ ├── users.service.ts # findByUsername, create, updateProfile
|
│ │ # 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
|
│ └── users.module.ts
|
||||||
├── categories/
|
├── categories/
|
||||||
│ ├── categories.controller.ts # GET /api/categories, GET /api/categories/tree (@Public)
|
│ ├── 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
|
### Användar-endpoints
|
||||||
```
|
```
|
||||||
POST /api/auth/login Logga in, returnerar JWT (@Public)
|
POST /api/auth/login Logga in, returnerar JWT inkl. role (@Public)
|
||||||
GET /api/users/me Hämta inloggad användares profil
|
GET /api/users/me Hämta inloggad användares profil (inkl. role)
|
||||||
PATCH /api/users/me Uppdatera firstName, lastName, email
|
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
|
### Baslager-endpoints
|
||||||
@@ -428,11 +441,23 @@ model User {
|
|||||||
firstName String?
|
firstName String?
|
||||||
lastName String?
|
lastName String?
|
||||||
passwordHash String
|
passwordHash String
|
||||||
|
role String @default("user") # "user" eller "admin"
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
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
|
### Category
|
||||||
```prisma
|
```prisma
|
||||||
model Category {
|
model Category {
|
||||||
@@ -495,16 +520,10 @@ model InventoryItem {
|
|||||||
unit String # Enhet (g, kg, ml, dl, st, tsk, msk, etc)
|
unit String # Enhet (g, kg, ml, dl, st, tsk, msk, etc)
|
||||||
brand String? # Varumärke
|
brand String? # Varumärke
|
||||||
location String? # Lagerplats (Kyl, Frys, Skafferi, etc)
|
location String? # Lagerplats (Kyl, Frys, Skafferi, etc)
|
||||||
priority Int? # Prioritetsordning
|
|
||||||
purchaseDate DateTime? # Köpdatum
|
purchaseDate DateTime? # Köpdatum
|
||||||
opened Boolean? # Markering för öppnad produkt
|
opened Boolean? # Markering för öppnad produkt
|
||||||
shelfNote String? # Lagringsnot
|
|
||||||
suitableFor String? # Lämplighetsmärkning (t.ex. "vegetarian")
|
suitableFor String? # Lämplighetsmärkning (t.ex. "vegetarian")
|
||||||
isOnSale Boolean? # Är på rea
|
|
||||||
priceLevel Int? # Priskategori (1–5)
|
|
||||||
bestBeforeDate DateTime? # Bäst före-datum
|
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
|
comment String? # Fri kommentar
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|||||||
@@ -283,3 +283,29 @@ INSERT IGNORE INTO `Category` (`name`, `parentId`)
|
|||||||
FROM `Category` c1
|
FROM `Category` c1
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Asien'
|
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Asien'
|
||||||
WHERE c1.name = 'Skafferi' AND c1.parentId IS NULL;
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user