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) |
|
||||
| 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')` 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) ✅
|
||||
**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
|
||||
- **Å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
@@ -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 (1–5)
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user