diff --git a/API.md b/API.md
new file mode 100644
index 00000000..fd5ff055
--- /dev/null
+++ b/API.md
@@ -0,0 +1,812 @@
+# Vet1177 API — Guide for frontend-utvecklare
+
+Denna guide visar hur du anropar backend-API:et från en React + Tailwind CSS-applikation.
+Alla exempel är skrivna för en djurägare (OWNER) som heter Anna.
+
+---
+
+## 1. Kom igang
+
+### Starta backend
+
+Backend startas i IntelliJ. Se till att Docker Desktop kor (behövs for databasen).
+Kör `Vet1177Application.java` — servern startar pa `http://localhost:8080`.
+
+### Inloggning under utveckling
+
+Just nu anvander vi en speciell header istället for riktigt login.
+Lagg till headern `X-Dev-User` med en e-postadress i varje request.
+Det simulerar att du ar inloggad som den anvandaren.
+
+**OBS:** Detta fungerar bara nar backend kör med dev-profilen (vilket ar default lokalt).
+
+### Testanvandare
+
+| Roll | Namn | Email |
+|-------|-----------------|-----------------|
+| OWNER | Anna Svensson | anna@test.se |
+| VET | Erik Veterinär | erik@klinik.se |
+| ADMIN | Sara Admin | sara@admin.se |
+
+Alla testanvandare har lösenordet `password` (hashat i databasen).
+
+---
+
+## 2. Hur man anropar API:et fran React
+
+### Grundmönster
+
+Alla anrop följer samma mönster:
+
+1. Anropa `fetch()` med rätt URL och headers
+2. Kolla att `response.ok` ar `true`
+3. Läs JSON-datan med `response.json()`
+
+```javascript
+// Grundmönster for att hamta data
+const hämtaData = async () => {
+ const response = await fetch('http://localhost:8080/api/ENDPOINT', {
+ headers: {
+ 'X-Dev-User': 'anna@test.se' // Talar om vem du ar
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error('Något gick fel: ' + response.status);
+ }
+
+ const data = await response.json();
+ return data;
+};
+```
+
+```javascript
+// Grundmönster for att skicka data (POST/PUT)
+const skickaData = async (body) => {
+ const response = await fetch('http://localhost:8080/api/ENDPOINT', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json', // Talar om att vi skickar JSON
+ 'X-Dev-User': 'anna@test.se'
+ },
+ body: JSON.stringify(body) // Gör om JavaScript-objekt till JSON-text
+ });
+
+ if (!response.ok) {
+ throw new Error('Något gick fel: ' + response.status);
+ }
+
+ const data = await response.json();
+ return data;
+};
+```
+
+---
+
+## 3. Endpoints for djurägare (OWNER)
+
+### 3.1 Hamta alla kliniker
+
+Anvands for att visa en dropdown nar djurägaren skapar ett nytt arende.
+Alla kliniker ar publika — ingen header behövs.
+
+**`GET /api/clinics`**
+
+```javascript
+const getKliniker = async () => {
+ const response = await fetch('http://localhost:8080/api/clinics');
+ const data = await response.json();
+ return data;
+};
+```
+
+**Svar (lista med kliniker):**
+
+```json
+[
+ {
+ "id": "a1b2c3d4-e5f6-4a5b-8c9d-e0f1a2b3c4d5",
+ "name": "Djurkliniken Centrum",
+ "address": "Storgatan 1, Stockholm",
+ "phoneNumber": "08-123456"
+ }
+]
+```
+
+---
+
+### 3.2 Hamta mina djur
+
+Visar alla djur som tillhör den inloggade djurägaren.
+
+**`GET /pets/owner/{ownerId}`**
+
+```javascript
+const getMinaDjur = async () => {
+ const ownerId = 'c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7'; // Annas ID
+
+ const response = await fetch(`http://localhost:8080/pets/owner/${ownerId}`, {
+ headers: {
+ 'X-Dev-User': 'anna@test.se'
+ }
+ });
+ const data = await response.json();
+ return data;
+};
+```
+
+**Svar (lista med djur):**
+
+```json
+[
+ {
+ "id": "f6a7b8c9-d0e1-4f5a-3b4c-d5e6f7a8b9c0",
+ "ownerId": "c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7",
+ "name": "Fido",
+ "species": "Hund",
+ "breed": "Labrador",
+ "dateOfBirth": "2020-01-15",
+ "weightKg": 25.5,
+ "createdAt": "2025-01-01T10:00:00Z",
+ "updatedAt": "2025-01-01T10:00:00Z"
+ },
+ {
+ "id": "a7b8c9d0-e1f2-4a5b-4c5d-e6f7a8b9c0d1",
+ "ownerId": "c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7",
+ "name": "Missan",
+ "species": "Katt",
+ "breed": "Persisk",
+ "dateOfBirth": "2019-06-20",
+ "weightKg": 4.2,
+ "createdAt": "2025-01-01T10:00:00Z",
+ "updatedAt": "2025-01-01T10:00:00Z"
+ }
+]
+```
+
+---
+
+### 3.3 Skapa ett djur
+
+Registrera ett nytt djur för den inloggade ägaren.
+
+**`POST /pets`**
+
+```javascript
+const skapaDjur = async () => {
+ const response = await fetch('http://localhost:8080/pets', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Dev-User': 'anna@test.se'
+ },
+ body: JSON.stringify({
+ name: 'Bella',
+ species: 'Hund',
+ breed: 'Golden Retriever',
+ dateOfBirth: '2022-03-10',
+ weightKg: 30.0
+ })
+ });
+ const data = await response.json();
+ return data;
+};
+```
+
+**Request body:**
+
+| Fält | Typ | Obligatoriskt | Beskrivning |
+|-------------|----------|---------------|---------------------------|
+| name | string | Ja | Djurets namn |
+| species | string | Ja | Art (Hund, Katt, etc.) |
+| breed | string | Nej | Ras |
+| dateOfBirth | string | Ja | Födelsedatum (YYYY-MM-DD) |
+| weightKg | number | Ja | Vikt i kg (positivt tal) |
+
+**Svar:** Samma format som i listan ovan (ett PetResponse-objekt).
+
+---
+
+### 3.4 Hamta mina ärenden
+
+Visar alla veterinärarenden som tillhör den inloggade djurägaren.
+
+**`GET /api/medical-records/my-records`**
+
+```javascript
+const getMinaÄrenden = async () => {
+ const response = await fetch('http://localhost:8080/api/medical-records/my-records', {
+ headers: {
+ 'X-Dev-User': 'anna@test.se'
+ }
+ });
+ const data = await response.json();
+ return data;
+};
+```
+
+**Svar (lista med ärenden i kortformat):**
+
+```json
+[
+ {
+ "id": "b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2",
+ "title": "Fido haltar",
+ "status": "OPEN",
+ "petName": "Fido",
+ "ownerName": "Anna Svensson",
+ "assignedVetName": null,
+ "createdAt": "2025-01-01T10:00:00Z"
+ },
+ {
+ "id": "c9d0e1f2-a3b4-4c5d-6e7f-a8b9c0d1e2f3",
+ "title": "Missan äter inte",
+ "status": "IN_PROGRESS",
+ "petName": "Missan",
+ "ownerName": "Anna Svensson",
+ "assignedVetName": "Erik Veterinär",
+ "createdAt": "2025-01-01T10:00:00Z"
+ }
+]
+```
+
+Möjliga statusar: `OPEN`, `IN_PROGRESS`, `AWAITING_INFO`, `CLOSED`
+
+---
+
+### 3.5 Skapa ett ärende
+
+Skapar ett nytt veterinärarende. Du behöver ett `petId` (fran 3.2) och ett `clinicId` (fran 3.1).
+
+**`POST /api/medical-records`**
+
+```javascript
+const skapaÄrende = async () => {
+ const response = await fetch('http://localhost:8080/api/medical-records', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Dev-User': 'anna@test.se'
+ },
+ body: JSON.stringify({
+ title: 'Fido hostar',
+ description: 'Hunden har hostat i tre dagar och verkar trött.',
+ petId: 'f6a7b8c9-d0e1-4f5a-3b4c-d5e6f7a8b9c0',
+ clinicId: 'a1b2c3d4-e5f6-4a5b-8c9d-e0f1a2b3c4d5'
+ })
+ });
+ const data = await response.json();
+ return data;
+};
+```
+
+**Request body:**
+
+| Fält | Typ | Obligatoriskt | Beskrivning |
+|-------------|--------|---------------|---------------------------------|
+| title | string | Ja | Kort titel (max 500 tecken) |
+| description | string | Nej | Beskrivning (max 5000 tecken) |
+| petId | string | Ja | UUID for djuret |
+| clinicId | string | Ja | UUID for kliniken |
+
+**Svar (fullstandigt arende):**
+
+```json
+{
+ "id": "nytt-uuid-här",
+ "title": "Fido hostar",
+ "description": "Hunden har hostat i tre dagar och verkar trött.",
+ "status": "OPEN",
+ "petId": "f6a7b8c9-d0e1-4f5a-3b4c-d5e6f7a8b9c0",
+ "petName": "Fido",
+ "petSpecies": "Hund",
+ "ownerId": "c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7",
+ "ownerName": "Anna Svensson",
+ "clinicId": "a1b2c3d4-e5f6-4a5b-8c9d-e0f1a2b3c4d5",
+ "clinicName": "Djurkliniken Centrum",
+ "assignedVetId": null,
+ "assignedVetName": null,
+ "createdById": "c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7",
+ "createdByName": "Anna Svensson",
+ "createdAt": "2025-04-14T10:00:00Z",
+ "updatedAt": null,
+ "closedAt": null
+}
+```
+
+---
+
+### 3.6 Hamta ett specifikt arende
+
+Visar alla detaljer for ett arende. Anvands nar djurägaren klickar pa ett arende i listan.
+
+**`GET /api/medical-records/{id}`**
+
+```javascript
+const getÄrende = async (ärendeId) => {
+ const response = await fetch(`http://localhost:8080/api/medical-records/${ärendeId}`, {
+ headers: {
+ 'X-Dev-User': 'anna@test.se'
+ }
+ });
+ const data = await response.json();
+ return data;
+};
+
+// Anvandning:
+const ärende = await getÄrende('b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2');
+```
+
+**Svar:** Samma format som i 3.5 (MedicalRecordResponse).
+
+---
+
+### 3.7 Skriva en kommentar
+
+Lagg till en kommentar pa ett arende. Tänk det som en chatt mellan djurägare och veterinär.
+
+**`POST /api/comments`**
+
+```javascript
+const skapaKommentar = async (recordId, text) => {
+ const response = await fetch('http://localhost:8080/api/comments', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Dev-User': 'anna@test.se'
+ },
+ body: JSON.stringify({
+ recordId: recordId,
+ body: text
+ })
+ });
+ const data = await response.json();
+ return data;
+};
+
+// Anvandning:
+await skapaKommentar('b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2', 'Fido verkar bättre idag!');
+```
+
+**Request body:**
+
+| Fält | Typ | Obligatoriskt | Beskrivning |
+|----------|--------|---------------|----------------------------------|
+| recordId | string | Ja | UUID for ärendet |
+| body | string | Ja | Kommentarstexten (max 5000 tecken) |
+
+**Svar:**
+
+```json
+{
+ "id": "nytt-uuid-här",
+ "recordId": "b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2",
+ "authorId": "c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7",
+ "authorName": "Anna Svensson",
+ "body": "Fido verkar bättre idag!",
+ "createdAt": "2025-04-14T10:00:00Z",
+ "updatedAt": null
+}
+```
+
+---
+
+### 3.8 Hamta kommentarer for ett arende
+
+Visar alla kommentarer pa ett arende, sorterade fran äldst till nyast.
+
+**`GET /api/comments/record/{recordId}`**
+
+```javascript
+const getKommentarer = async (recordId) => {
+ const response = await fetch(`http://localhost:8080/api/comments/record/${recordId}`, {
+ headers: {
+ 'X-Dev-User': 'anna@test.se'
+ }
+ });
+ const data = await response.json();
+ return data;
+};
+
+// Anvandning:
+const kommentarer = await getKommentarer('b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2');
+```
+
+**Svar (lista med kommentarer):**
+
+```json
+[
+ {
+ "id": "kommentar-uuid",
+ "recordId": "b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2",
+ "authorId": "c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7",
+ "authorName": "Anna Svensson",
+ "body": "Fido verkar bättre idag!",
+ "createdAt": "2025-04-14T10:00:00Z",
+ "updatedAt": null
+ }
+]
+```
+
+---
+
+### 3.9 Bifoga en fil
+
+Ladda upp en bild eller PDF till ett arende. Bilagor skickas som `multipart/form-data` — inte JSON.
+Det ar samma format som ett vanligt HTML-formulär med ``.
+
+**`POST /api/attachments/record/{recordId}`**
+
+```javascript
+const laddaUppFil = async (recordId, file, beskrivning) => {
+ // FormData anvands istället for JSON nar man skickar filer
+ const formData = new FormData();
+ formData.append('file', file); // file = File-objekt fran
+ formData.append('description', beskrivning); // Valfri beskrivning
+
+ const response = await fetch(`http://localhost:8080/api/attachments/record/${recordId}`, {
+ method: 'POST',
+ headers: {
+ 'X-Dev-User': 'anna@test.se'
+ // OBS: Satt INTE 'Content-Type' här — browsern lägger till det automatiskt
+ // med rätt boundary for multipart/form-data
+ },
+ body: formData // Skicka FormData direkt, INTE JSON.stringify()
+ });
+ const data = await response.json();
+ return data;
+};
+
+// Anvandning i en React-komponent:
+// laddaUppFil(recordId, e.target.files[0], 'Rontgenbild')} />
+```
+
+**Tillåtna filtyper:** JPG, PNG, PDF
+**Maxstorlek:** 10 MB
+
+**Svar (201 Created):**
+
+```json
+{
+ "id": "bilaga-uuid",
+ "recordId": "b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2",
+ "fileName": "rontgen_fido.jpg",
+ "description": "Rontgenbild",
+ "fileType": "image/jpeg",
+ "fileSizeBytes": 245000,
+ "uploadedAt": "2025-04-14T10:00:00Z",
+ "uploadedBy": "Anna Svensson",
+ "downloadUrl": "https://minio.../presigned-url"
+}
+```
+
+---
+
+### 3.10 Hamta bilagor for ett arende
+
+Visar alla uppladdade filer pa ett arende. Varje bilaga har en `downloadUrl` som du kan anvanda som `src` i en `
` eller som `href` i en ``.
+
+**`GET /api/attachments/record/{recordId}`**
+
+```javascript
+const getBilagor = async (recordId) => {
+ const response = await fetch(`http://localhost:8080/api/attachments/record/${recordId}`, {
+ headers: {
+ 'X-Dev-User': 'anna@test.se'
+ }
+ });
+ const data = await response.json();
+ return data;
+};
+```
+
+**Svar (lista med bilagor):**
+
+```json
+[
+ {
+ "id": "bilaga-uuid",
+ "recordId": "b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2",
+ "fileName": "rontgen_fido.jpg",
+ "description": "Rontgenbild",
+ "fileType": "image/jpeg",
+ "fileSizeBytes": 245000,
+ "uploadedAt": "2025-04-14T10:00:00Z",
+ "uploadedBy": "Anna Svensson",
+ "downloadUrl": "https://minio.../presigned-url"
+ }
+]
+```
+
+**Tips:** `downloadUrl` ar en tidsbegränsad länk (giltig i 15 minuter). Om bilden inte laddas, hamta bilagorna igen for att fa en ny URL.
+
+---
+
+## 4. Statuskoder att känna till
+
+Nar du anropar API:et far du alltid tillbaka en HTTP-statuskod. Kolla den i `response.status`.
+
+| Kod | Namn | Vad det betyder |
+|-----|------------------|--------------------------------------------------------------|
+| 200 | OK | Allt fungerade. Datan finns i response body. |
+| 201 | Created | Resursen skapades. Datan finns i response body. |
+| 204 | No Content | Lyckades, men inget data i svaret (t.ex. vid delete). |
+| 400 | Bad Request | Något ar fel i din request. Kolla att alla fält ar ifyllda. |
+| 403 | Forbidden | Du har inte rättighet. T.ex. en OWNER som försöker nå en VET-endpoint. |
+| 404 | Not Found | Resursen finns inte. Kolla att UUID:t stämmer. |
+| 500 | Server Error | Något gick fel i backend. Kolla IntelliJ-loggen. |
+
+**Felmeddelanden** kommer som JSON:
+
+```json
+{
+ "status": 400,
+ "message": "Validation failed",
+ "errors": {
+ "title": ["Titel far inte vara tom"]
+ }
+}
+```
+
+---
+
+## 5. Nar JWT ar implementerat
+
+Just nu anvander vi `X-Dev-User`-headern for att simulera inloggning. Nar JWT ar klart byter du ut den mot en riktig token.
+
+### Före (nu — utvecklingsläge)
+
+```javascript
+headers: {
+ 'X-Dev-User': 'anna@test.se'
+}
+```
+
+### Efter (med JWT)
+
+```javascript
+// 1. Logga in och fa en token
+const login = async (email, password) => {
+ const response = await fetch('http://localhost:8080/api/auth/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, password })
+ });
+ const data = await response.json();
+ return data.token; // Spara denna i state eller localStorage
+};
+
+// 2. Anvand token i alla andra anrop
+headers: {
+ 'Authorization': `Bearer ${token}`
+}
+```
+
+**Allt annat ar likadant** — samma URLer, samma request bodies, samma svar. Bara headern andras.
+
+---
+
+## 6. Kända test-UUID:n
+
+Dessa UUID:n finns i testdatan som laddas vid uppstart. Anvand dem for att testa utan att behöva skapa data först.
+
+### Klinik
+
+| Namn | UUID |
+|-----------------------|----------------------------------------|
+| Djurkliniken Centrum | `a1b2c3d4-e5f6-4a5b-8c9d-e0f1a2b3c4d5` |
+
+### Anvandare
+
+| Namn | Roll | Email | UUID |
+|-----------------|-------|----------------|----------------------------------------|
+| Anna Svensson | OWNER | anna@test.se | `c3d4e5f6-a7b8-4c5d-0e1f-a2b3c4d5e6f7` |
+| Erik Veterinär | VET | erik@klinik.se | `d4e5f6a7-b8c9-4d5e-1f2a-b3c4d5e6f7a8` |
+| Sara Admin | ADMIN | sara@admin.se | `e5f6a7b8-c9d0-4e5f-2a3b-c4d5e6f7a8b9` |
+
+### Djur
+
+| Namn | Art | Ras | UUID |
+|--------|------|-----------|----------------------------------------|
+| Fido | Hund | Labrador | `f6a7b8c9-d0e1-4f5a-3b4c-d5e6f7a8b9c0` |
+| Missan | Katt | Persisk | `a7b8c9d0-e1f2-4a5b-4c5d-e6f7a8b9c0d1` |
+
+### Arenden
+
+| Titel | Status | UUID |
+|-----------------|-------------|----------------------------------------|
+| Fido haltar | OPEN | `b8c9d0e1-f2a3-4b5c-5d6e-f7a8b9c0d1e2` |
+| Missan äter inte | IN_PROGRESS | `c9d0e1f2-a3b4-4c5d-6e7f-a8b9c0d1e2f3` |
+
+---
+
+## 7. Alla endpoints — komplett lista
+
+Nedan ar samtliga endpoints i API:et, grupperade per controller.
+Kolumnen "Auth" visar vem som kan anropa endpointen.
+
+**Förkortningar:**
+- **Alla** = ingen inloggning krävs (permitAll)
+- **Inloggad** = alla inloggade anvandare
+- **OWNER** = djurägare
+- **VET** = veterinär
+- **ADMIN** = administratör
+- **Policy** = rollkontroll sker i service/policy-lagret, inte i controllern
+
+### Autentisering (`/api/auth`)
+
+*Dessa endpoints finns ännu inte — de skapas av Person B (se issue #1–3).*
+
+| Metod | URL | Auth | Beskrivning | Request body |
+|-------|-----|------|-------------|--------------|
+| POST | `/api/auth/register` | Alla | Registrera ny anvandare | `{ name, email, password, role }` |
+| POST | `/api/auth/login` | Alla | Logga in, fa JWT-token | `{ email, password }` |
+
+### Kliniker (`/api/clinics`)
+
+| Metod | URL | Auth | Beskrivning | Request body |
+|-------|-----|------|-------------|--------------|
+| GET | `/api/clinics` | Alla | Hamta alla kliniker | — |
+| GET | `/api/clinics/{id}` | Alla | Hamta en klinik | — |
+| POST | `/api/clinics` | Inloggad | Skapa klinik | `{ name, address, phoneNumber }` |
+| PUT | `/api/clinics/{id}` | Inloggad | Uppdatera klinik | `{ name, address, phoneNumber }` |
+| DELETE | `/api/clinics/{id}` | Inloggad | Ta bort klinik | — |
+
+**Svar (ClinicResponse):**
+```json
+{ "id": "uuid", "name": "...", "address": "...", "phoneNumber": "..." }
+```
+
+### Djur (`/pets`)
+
+| Metod | URL | Auth | Beskrivning | Request body |
+|-------|-----|------|-------------|--------------|
+| POST | `/pets` | OWNER/ADMIN (Policy) | Skapa djur. Admin kan ange `?ownerId=uuid` | `{ name, species, breed, dateOfBirth, weightKg }` |
+| GET | `/pets/{petId}` | OWNER/VET/ADMIN (Policy) | Hamta ett djur | — |
+| GET | `/pets/owner/{ownerId}` | OWNER/ADMIN (Policy) | Hamta alla djur for en ägare | — |
+| PUT | `/pets/{petId}` | OWNER/ADMIN (Policy) | Uppdatera djurinfo | `{ name, species, breed, dateOfBirth, weightKg }` |
+| DELETE | `/pets/{petId}` | OWNER/ADMIN (Policy) | Ta bort djur | — |
+
+**Svar (PetResponse):**
+```json
+{
+ "id": "uuid", "ownerId": "uuid", "name": "Fido", "species": "Hund",
+ "breed": "Labrador", "dateOfBirth": "2020-01-15", "weightKg": 25.5,
+ "createdAt": "2025-01-01T10:00:00Z", "updatedAt": "2025-01-01T10:00:00Z"
+}
+```
+
+### Arenden (`/api/medical-records`)
+
+| Metod | URL | Auth | Beskrivning | Request body |
+|-------|-----|------|-------------|--------------|
+| POST | `/api/medical-records` | Inloggad (Policy) | Skapa arende | `{ title, description, petId, clinicId }` |
+| GET | `/api/medical-records/{id}` | Inloggad (Policy) | Hamta ett arende (fullstandig) | — |
+| GET | `/api/medical-records/my-records` | OWNER | Hamta mina ärenden (kortformat) | — |
+| GET | `/api/medical-records/owner/{ownerId}` | OWNER (egna)/VET/ADMIN | Hamta ärenden per ägare | — |
+| GET | `/api/medical-records/pet/{petId}` | Inloggad (Policy) | Hamta ärenden per djur | — |
+| GET | `/api/medical-records/clinic/{clinicId}` | VET/ADMIN (Policy) | Hamta ärenden per klinik | — |
+| GET | `/api/medical-records/clinic/{clinicId}/status/{status}` | VET/ADMIN (Policy) | Hamta ärenden per klinik + status | — |
+| PUT | `/api/medical-records/{id}` | VET/ADMIN (Policy) | Uppdatera titel/beskrivning | `{ title, description }` |
+| PUT | `/api/medical-records/{id}/assign-vet` | VET/ADMIN (Policy) | Tilldela veterinär | `{ vetId }` |
+| PUT | `/api/medical-records/{id}/status` | VET/ADMIN (Policy) | Ändra status | `{ status }` |
+| PUT | `/api/medical-records/{id}/close` | VET/ADMIN (Policy) | Stäng ärende | — |
+
+**Möjliga statusar:** `OPEN`, `IN_PROGRESS`, `AWAITING_INFO`, `CLOSED`
+
+**Svar — fullständigt (MedicalRecordResponse):**
+```json
+{
+ "id": "uuid", "title": "...", "description": "...", "status": "OPEN",
+ "petId": "uuid", "petName": "Fido", "petSpecies": "Hund",
+ "ownerId": "uuid", "ownerName": "Anna Svensson",
+ "clinicId": "uuid", "clinicName": "Djurkliniken Centrum",
+ "assignedVetId": null, "assignedVetName": null,
+ "createdById": "uuid", "createdByName": "Anna Svensson",
+ "createdAt": "2025-01-01T10:00:00Z", "updatedAt": null, "closedAt": null
+}
+```
+
+**Svar — kortformat (MedicalRecordSummaryResponse):**
+```json
+{
+ "id": "uuid", "title": "...", "status": "OPEN",
+ "petName": "Fido", "ownerName": "Anna Svensson",
+ "assignedVetName": null, "createdAt": "2025-01-01T10:00:00Z"
+}
+```
+
+### Kommentarer (`/api/comments`)
+
+| Metod | URL | Auth | Beskrivning | Request body |
+|-------|-----|------|-------------|--------------|
+| POST | `/api/comments` | Inloggad (Policy) | Skapa kommentar | `{ recordId, body }` |
+| GET | `/api/comments/record/{recordId}` | Inloggad (Policy) | Hamta kommentarer for ett arende | — |
+| GET | `/api/comments/record/{recordId}/count` | Inloggad (Policy) | Antal kommentarer pa ett arende | — |
+| PUT | `/api/comments/{id}` | Inloggad (Policy) | Uppdatera kommentar | `{ body }` |
+| DELETE | `/api/comments/{id}` | Inloggad (Policy) | Ta bort kommentar | — |
+
+**Svar (CommentResponse):**
+```json
+{
+ "id": "uuid", "recordId": "uuid", "authorId": "uuid",
+ "authorName": "Anna Svensson", "body": "Fido verkar bättre!",
+ "createdAt": "2025-04-14T10:00:00Z", "updatedAt": null
+}
+```
+
+### Bilagor (`/api/attachments`)
+
+| Metod | URL | Auth | Beskrivning | Request body |
+|-------|-----|------|-------------|--------------|
+| POST | `/api/attachments/record/{recordId}` | Inloggad (Policy) | Ladda upp fil (multipart/form-data) | `file` + valfri `description` |
+| GET | `/api/attachments/record/{recordId}` | Inloggad (Policy) | Hamta bilagor for ett arende | — |
+| GET | `/api/attachments/{id}/download` | Inloggad (Policy) | Hamta en bilaga (presigned URL) | — |
+| DELETE | `/api/attachments/{id}` | VET/ADMIN (Policy) | Ta bort bilaga | — |
+
+**Tillåtna filtyper:** JPG, PNG, PDF. **Max:** 10 MB.
+
+**Svar (AttachmentResponse):**
+```json
+{
+ "id": "uuid", "recordId": "uuid", "fileName": "bild.jpg",
+ "description": "Rontgenbild", "fileType": "image/jpeg",
+ "fileSizeBytes": 245000, "uploadedAt": "2025-04-14T10:00:00Z",
+ "uploadedBy": "Anna Svensson", "downloadUrl": "https://presigned-url..."
+}
+```
+
+### Aktivitetslogg (`/api/activity-logs`)
+
+| Metod | URL | Auth | Beskrivning | Request body |
+|-------|-----|------|-------------|--------------|
+| GET | `/api/activity-logs/record/{recordId}` | Inloggad (Policy) | Hamta händelselogg for ett arende | — |
+
+**Svar (lista med ActivityLogResponse):**
+```json
+[
+ {
+ "id": "uuid", "action": "CASE_CREATED", "description": "Ärende skapat",
+ "performedById": "uuid", "performedByName": "Anna Svensson",
+ "recordId": "uuid", "createdAt": "2025-01-01T10:00:00Z"
+ }
+]
+```
+
+**Möjliga actions:** `CASE_CREATED`, `UPDATED`, `STATUS_CHANGED`, `ASSIGNED`, `COMMENT_ADDED`
+
+### Anvandare (`/api/users`)
+
+| Metod | URL | Auth | Beskrivning | Request body |
+|-------|-----|------|-------------|--------------|
+| GET | `/api/users` | Inloggad | Hamta alla anvandare | — |
+| GET | `/api/users/{id}` | Inloggad | Hamta en anvandare | — |
+| POST | `/api/users` | Inloggad | Skapa anvandare | `{ name, email, password, role, clinicId? }` |
+| PUT | `/api/users/{id}` | Inloggad | Uppdatera anvandare | `{ name?, email?, clinicId? }` |
+| DELETE | `/api/users/{id}` | Inloggad | Ta bort anvandare | — |
+
+**Svar (UserResponse):**
+```json
+{
+ "id": "uuid", "name": "Anna Svensson", "email": "anna@test.se",
+ "role": "OWNER", "clinicId": null,
+ "createdAt": "2025-01-01T10:00:00Z", "updatedAt": "2025-01-01T10:00:00Z"
+}
+```
+
+### Veterinärer (`/api/vets`)
+
+| Metod | URL | Auth | Beskrivning | Request body |
+|-------|-----|------|-------------|--------------|
+| GET | `/api/vets` | Inloggad | Hamta alla veterinärer | — |
+| GET | `/api/vets/{id}` | Inloggad | Hamta en veterinär | — |
+| POST | `/api/vets` | ADMIN | Skapa veterinärprofil | `{ userId, licenseId, specialization?, bookingInfo? }` |
+
+**Svar (VetResponse):**
+```json
+{
+ "userId": "uuid", "name": "Erik Veterinär", "email": "erik@klinik.se",
+ "licenseId": "VET-001", "specialization": "Ortopedi",
+ "bookingInfo": "Mån-Fre 08-17", "clinicName": "Djurkliniken Centrum",
+ "isActive": true
+}
diff --git a/pom.xml b/pom.xml
index 92752b0e..6ec7ad1e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -81,6 +81,10 @@
org.springframework.boot
spring-boot-starter-security
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
software.amazon.awssdk
s3
diff --git a/src/main/java/org/example/vet1177/controller/ActivityLogController.java b/src/main/java/org/example/vet1177/controller/ActivityLogController.java
index 5086b488..898dc1b8 100644
--- a/src/main/java/org/example/vet1177/controller/ActivityLogController.java
+++ b/src/main/java/org/example/vet1177/controller/ActivityLogController.java
@@ -3,9 +3,9 @@
import org.example.vet1177.dto.response.activitylog.ActivityLogResponse;
import org.example.vet1177.entities.User;
import org.example.vet1177.services.ActivityLogService;
-import org.example.vet1177.services.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -18,26 +18,22 @@ public class ActivityLogController {
private static final Logger log = LoggerFactory.getLogger(ActivityLogController.class);
private final ActivityLogService activityLogService;
- private final UserService userService;
- public ActivityLogController(ActivityLogService activityLogService,
- UserService userService) {
+ public ActivityLogController(ActivityLogService activityLogService) {
this.activityLogService = activityLogService;
- this.userService = userService;
}
// Hämta alla logs för ett MedicalRecord
@GetMapping("/record/{recordId}")
public List getLogsByRecord(
@PathVariable UUID recordId,
- @RequestHeader("userId") UUID userId
+ @AuthenticationPrincipal User currentUser
) {
log.info("GET /api/activity-logs/record/{}", recordId);
- User user = userService.getUserEntityById(userId);
- return activityLogService.getByRecord(recordId, user)
+ return activityLogService.getByRecord(recordId, currentUser)
.stream()
.map(ActivityLogResponse::from)
.toList();
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/example/vet1177/controller/PetController.java b/src/main/java/org/example/vet1177/controller/PetController.java
index 0c808159..99677543 100644
--- a/src/main/java/org/example/vet1177/controller/PetController.java
+++ b/src/main/java/org/example/vet1177/controller/PetController.java
@@ -8,8 +8,8 @@
import org.example.vet1177.services.PetService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.example.vet1177.services.UserService;
import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -22,44 +22,42 @@ public class PetController {
private static final Logger log = LoggerFactory.getLogger(PetController.class);
private final PetService petService;
- private final UserService userService;
- public PetController(PetService petService, UserService userService) {
+ public PetController(PetService petService) {
this.petService = petService;
- this.userService = userService;
}
- //POST / pets - skapa nytt djur
+ //POST /pets - skapa nytt djur
@PostMapping
public PetResponse createPet(
- // TODO: Ersätt med användare från autentiserad kontext (t.ex. JWT / Spring Security)
- @RequestHeader UUID currentUserId,
+ @AuthenticationPrincipal User currentUser,
@RequestParam(required = false) UUID ownerId,
@Valid @RequestBody PetRequest request
) {
- log.info("POST /pets - creating pet for currentUserId={} ownerId={}", currentUserId, ownerId);
- Pet saved = petService.createPet(currentUserId, ownerId, request);
+ log.info("POST /pets - creating pet for userId={} ownerId={}", currentUser.getId(), ownerId);
+ Pet saved = petService.createPet(currentUser.getId(), ownerId, request);
return toResponse(saved);
}
- // GET / pets/{petId} - hämta ett specifikt djur
+ // GET /pets/{petId} - hämta ett specifikt djur
@GetMapping("/{petId}")
public ResponseEntity getPetById(
- @RequestHeader UUID currentUserId,
+ @AuthenticationPrincipal User currentUser,
@PathVariable UUID petId
) {
- User currentUser = userService.getUserEntityById(currentUserId);
+ log.info("GET /pets/{}", petId);
Pet pet = petService.getPetById(petId, currentUser);
return ResponseEntity.ok(toResponse(pet));
}
- // GET/pets/owner/{ownerId} - hämta alla djur för en ägare
+ // GET /pets/owner/{ownerId} - hämta alla djur för en ägare
@GetMapping("/owner/{ownerId}")
public ResponseEntity> getPetsByOwner(
- @RequestHeader UUID currentUserId,
+ @AuthenticationPrincipal User currentUser,
@PathVariable UUID ownerId
) {
- List pets = petService.getPetsByOwner(currentUserId, ownerId)
+ log.info("GET /pets/owner/{}", ownerId);
+ List pets = petService.getPetsByOwner(currentUser.getId(), ownerId)
.stream()
.map(this::toResponse)
.toList();
@@ -69,21 +67,22 @@ public ResponseEntity> getPetsByOwner(
// PUT /pets/{petId} - uppdatera ett djur
@PutMapping("/{petId}")
public ResponseEntity updatePet(
- @RequestHeader UUID currentUserId,
+ @AuthenticationPrincipal User currentUser,
@PathVariable UUID petId,
@Valid @RequestBody PetRequest request
) {
- Pet updated = petService.updatePet(currentUserId, petId, request);
+ log.info("PUT /pets/{}", petId);
+ Pet updated = petService.updatePet(currentUser.getId(), petId, request);
return ResponseEntity.ok(toResponse(updated));
}
// DELETE /pets/{petId} - radera ett djur
@DeleteMapping("/{petId}")
public ResponseEntity deletePet(
- @RequestHeader UUID currentUserId,
+ @AuthenticationPrincipal User currentUser,
@PathVariable UUID petId
) {
- User currentUser = userService.getUserEntityById(currentUserId);
+ log.info("DELETE /pets/{}", petId);
petService.deletePet(petId, currentUser);
return ResponseEntity.noContent().build();
}
@@ -103,4 +102,4 @@ private PetResponse toResponse(Pet pet) {
);
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/example/vet1177/security/CustomUserDetailsService.java b/src/main/java/org/example/vet1177/security/CustomUserDetailsService.java
new file mode 100644
index 00000000..3251b4ed
--- /dev/null
+++ b/src/main/java/org/example/vet1177/security/CustomUserDetailsService.java
@@ -0,0 +1,50 @@
+package org.example.vet1177.security;
+
+import org.example.vet1177.repository.UserRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+/**
+ * Bryggan mellan Spring Security och vår databas.
+ *
+ * Spring Security behöver veta hur man hämtar en användare givet ett "användarnamn".
+ * I vårt fall är "användarnamnet" email-adressen (det som står i JWT:ns subject-claim).
+ *
+ * Flödet:
+ * JWT-token innehåller sub="sara@vet.se"
+ * ↓
+ * JwtAuthenticationFilter anropar loadUserByUsername("sara@vet.se")
+ * ↓
+ * Vi söker i databasen: userRepository.findByEmail("sara@vet.se")
+ * ↓
+ * Returnerar User-objektet (som implementerar UserDetails)
+ * ↓
+ * Spring Security sätter det i SecurityContext → @AuthenticationPrincipal fungerar
+ */
+@Service
+public class CustomUserDetailsService implements UserDetailsService {
+
+ private static final Logger log = LoggerFactory.getLogger(CustomUserDetailsService.class);
+
+ private final UserRepository userRepository;
+
+ public CustomUserDetailsService(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ }
+
+ @Override
+ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
+ log.debug("Loading user by email={}", email);
+
+
+ return userRepository.findByEmail(email)
+ .orElseThrow(() -> {
+ log.warn("User not found email={}", email);
+ return new UsernameNotFoundException("Användare hittades inte: " + email);
+ });
+ }
+}
diff --git a/src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java b/src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java
new file mode 100644
index 00000000..519eb079
--- /dev/null
+++ b/src/main/java/org/example/vet1177/security/JwtAuthenticationFilter.java
@@ -0,0 +1,128 @@
+package org.example.vet1177.security;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.oauth2.jwt.JwtException;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+/**
+ * Filter som körs EN gång per HTTP-request (OncePerRequestFilter).
+ *
+ * Vad händer steg för steg:
+ *
+ * [Klient] → HTTP Request med "Authorization: Bearer eyJhbG..."
+ * ↓
+ * [JwtAuthenticationFilter]
+ * 1. Läser Authorization-headern
+ * 2. Extraherar token-strängen (allt efter "Bearer ")
+ * 3. Avkodar token via JwtService → får email + roll
+ * 4. Laddar User från DB via CustomUserDetailsService
+ * 5. Skapar ett Authentication-objekt och sätter det i SecurityContext
+ * ↓
+ * [Spring Security]
+ * Kollar SecurityContext: finns en autentiserad användare?
+ * Om ja → kontrollerar att användaren har rätt roll för endpointen
+ * Om nej → returnerar 401 Unauthorized
+ * ↓
+ * [Controller]
+ * @AuthenticationPrincipal User currentUser ← hämtas från SecurityContext
+ */
+@Component
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
+
+ private final JwtService jwtService;
+ private final CustomUserDetailsService userDetailsService;
+
+ public JwtAuthenticationFilter(JwtService jwtService, CustomUserDetailsService userDetailsService) {
+ this.jwtService = jwtService;
+ this.userDetailsService = userDetailsService;
+ }
+
+ @Override
+ protected void doFilterInternal(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+
+ // 1. Läs Authorization-headern
+ // Formatet är: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+ String authHeader = request.getHeader("Authorization");
+
+ // Om headern saknas eller inte börjar med "Bearer " → skippa filtret.
+ // Requesten släpps vidare i kedjan. Om endpointen kräver auth
+ // kommer Spring Security att returnera 401 automatiskt.
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ // 2. Extrahera token-strängen (ta bort "Bearer " som är 7 tecken)
+ String token = authHeader.substring(7);
+
+ try {
+ // 3. Avkoda och validera token.
+ // Om signaturen inte stämmer eller token har gått ut kastar decode() JwtException.
+ Jwt jwt = jwtService.decode(token);
+
+ // 4. Hämta email från token (subject-claim)
+ String email = jwt.getSubject();
+
+ // Kolla att vi inte redan har autentiserat denna request
+ // (kan hända om flera filter kör i kedjan)
+ if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+
+ // 5. Ladda det fulla User-objektet från databasen.
+ // Vi behöver hela entiteten (med id, klinik, roll) för @AuthenticationPrincipal.
+ var userDetails = userDetailsService.loadUserByUsername(email);
+
+ // 6. Skapa ett Authentication-objekt.
+ // UsernamePasswordAuthenticationToken är Springs standardklass för "en autentiserad användare".
+ // - Första argumentet (principal) → User-objektet (det som @AuthenticationPrincipal ger)
+ // - Andra argumentet (credentials) → null (vi behöver inte lösenordet, token räcker)
+ // - Tredje argumentet (authorities) → rollerna (ROLE_VET, ROLE_ADMIN, etc.)
+ var authentication = new UsernamePasswordAuthenticationToken(
+ userDetails, null, userDetails.getAuthorities()
+ );
+
+ // Kopplar request-detaljer (IP-adress, session-id) till autentiseringen — för loggning.
+ authentication.setDetails(
+ new WebAuthenticationDetailsSource().buildDetails(request)
+ );
+
+ // 7. Sätt autentiseringen i SecurityContext.
+ // Från och med nu "vet" Spring Security att det finns en inloggad användare.
+ // Alla controllers kan hämta den via @AuthenticationPrincipal.
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ log.debug("Authenticated user email={} via JWT", email);
+ }
+
+ } catch (JwtException e) {
+ // Token är ogiltig (felaktig signatur, utgången, korrupt).
+ // Vi loggar och släpper vidare — Spring Security returnerar 401 automatiskt
+ // eftersom SecurityContext förblir tom.
+ log.warn("Invalid JWT token: {}", e.getMessage());
+ } catch (UsernameNotFoundException e) {
+ // Token är giltig men användaren finns inte längre i databasen
+ // (t.ex. raderad av admin medan tokenen fortfarande gäller).
+ log.warn("User from JWT no longer exists: {}", e.getMessage());
+ }
+
+ // Släpp vidare requesten till nästa filter i kedjan (och sedan till controllern)
+ filterChain.doFilter(request, response);
+ }
+}
diff --git a/src/main/java/org/example/vet1177/security/JwtProperties.java b/src/main/java/org/example/vet1177/security/JwtProperties.java
new file mode 100644
index 00000000..1c165f55
--- /dev/null
+++ b/src/main/java/org/example/vet1177/security/JwtProperties.java
@@ -0,0 +1,22 @@
+package org.example.vet1177.security;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Läser JWT-konfiguration från application.properties.
+ *
+ * jwt.secret-key → den hemliga nyckeln som används för att signera/verifiera tokens (HMAC SHA-256)
+ * jwt.expiration-ms → hur länge en token är giltig i millisekunder (86400000 = 24 timmar)
+ */
+@ConfigurationProperties(prefix = "jwt")
+public class JwtProperties {
+
+ private String secretKey;
+ private long expirationMs;
+
+ public String getSecretKey() { return secretKey; }
+ public void setSecretKey(String secretKey) { this.secretKey = secretKey; }
+
+ public long getExpirationMs() { return expirationMs; }
+ public void setExpirationMs(long expirationMs) { this.expirationMs = expirationMs; }
+}
diff --git a/src/main/java/org/example/vet1177/security/JwtService.java b/src/main/java/org/example/vet1177/security/JwtService.java
new file mode 100644
index 00000000..6643c970
--- /dev/null
+++ b/src/main/java/org/example/vet1177/security/JwtService.java
@@ -0,0 +1,115 @@
+package org.example.vet1177.security;
+
+import com.nimbusds.jose.jwk.source.ImmutableSecret;
+import org.example.vet1177.entities.User;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
+import org.springframework.security.oauth2.jwt.*;
+import org.springframework.stereotype.Service;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+
+/**
+ * Hanterar skapande och validering av JWT-tokens.
+ *
+ * Flödet:
+ * 1. Användaren loggar in med email + lösenord
+ * 2. AuthService anropar generateToken(user) → får tillbaka en JWT-sträng
+ * 3. Frontend skickar JWT:n i varje request: "Authorization: Bearer "
+ * 4. JwtAuthenticationFilter fångar headern, anropar decode(token) → får tillbaka claims
+ * 5. Filtret laddar användaren från DB och sätter den i SecurityContext
+ *
+ * Vi använder HS256 (HMAC med SHA-256) — en symmetrisk algoritm där samma
+ * hemliga nyckel används för att både signera och verifiera tokens.
+ */
+@Service
+public class JwtService {
+
+ private static final Logger log = LoggerFactory.getLogger(JwtService.class);
+
+ private final JwtEncoder jwtEncoder;
+ private final JwtDecoder jwtDecoder;
+ private final long expirationMs;
+
+ public JwtService(JwtProperties jwtProperties) {
+ byte[] keyBytes = jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8);
+ if (keyBytes.length < 32) {
+ throw new IllegalStateException(
+ "JWT secret key must be at least 32 bytes for HS256, got " + keyBytes.length);
+ }
+
+ SecretKey key = new SecretKeySpec(keyBytes, "HmacSHA256");
+
+ // JwtEncoder — skapar nya tokens.
+ // ImmutableSecret wrapprar vår nyckel så att Nimbus-biblioteket kan använda den.
+ this.jwtEncoder = new NimbusJwtEncoder(new ImmutableSecret<>(key));
+
+ // JwtDecoder — avkodar och validerar befintliga tokens.
+ // Den kontrollerar automatiskt: signatur (är tokenen äkta?) och utgångstid (har den gått ut?).
+ this.jwtDecoder = NimbusJwtDecoder.withSecretKey(key)
+ .macAlgorithm(MacAlgorithm.HS256)
+ .build();
+
+ this.expirationMs = jwtProperties.getExpirationMs();
+ }
+
+ /**
+ * Skapar en ny JWT för en inloggad användare.
+ *
+ * Tokenen innehåller "claims" — nyckel-värde-par med information:
+ * - sub (subject): användarens email — det unika identifieringsvärdet
+ * - userId: UUID:t — behövs för att hämta användaren från DB
+ * - role: ROLE_VET / ROLE_OWNER / ROLE_ADMIN — för rollbaserad åtkomstkontroll
+ * - iat (issued at): när tokenen skapades
+ * - exp (expires at): när tokenen slutar gälla
+ */
+ public String generateToken(User user) {
+ Instant now = Instant.now();
+
+ JwtClaimsSet claims = JwtClaimsSet.builder()
+ .subject(user.getEmail())
+ .claim("userId", user.getId().toString())
+ .claim("role", "ROLE_" + user.getRole().name())
+ .issuedAt(now)
+ .expiresAt(now.plusMillis(expirationMs))
+ .build();
+
+ // Talar om att vi vill signera med HS256-algoritmen
+ JwtEncoderParameters params = JwtEncoderParameters.from(
+ JwsHeader.with(MacAlgorithm.HS256).build(),
+ claims
+ );
+
+ String token = jwtEncoder.encode(params).getTokenValue();
+ log.info("Generated JWT for user email={}", user.getEmail());
+ return token;
+ }
+
+ /**
+ * Avkodar och validerar en JWT-sträng.
+ *
+ * Nimbus-biblioteket gör automatiskt:
+ * 1. Kontrollerar att signaturen stämmer (ingen har ändrat innehållet)
+ * 2. Kontrollerar att tokenen inte har gått ut (exp > nu)
+ *
+ * Om något är fel kastas JwtException och anroparen vet att tokenen är ogiltig.
+ *
+ * @return Jwt-objekt med alla claims om tokenen är giltig
+ * @throws JwtException om tokenen är ogiltig, manipulerad eller utgången
+ */
+ public Jwt decode(String token) {
+ return jwtDecoder.decode(token);
+ }
+
+ /**
+ * Exponerar JwtDecoder som en bean som Spring Security kan använda.
+ * SecurityConfig behöver en JwtDecoder-bean för att konfigurera oauth2ResourceServer().
+ */
+ public JwtDecoder getJwtDecoder() {
+ return jwtDecoder;
+ }
+}
diff --git a/src/main/java/org/example/vet1177/security/SecurityConfig.java b/src/main/java/org/example/vet1177/security/SecurityConfig.java
index 3f867e90..377048b2 100644
--- a/src/main/java/org/example/vet1177/security/SecurityConfig.java
+++ b/src/main/java/org/example/vet1177/security/SecurityConfig.java
@@ -1,28 +1,149 @@
package org.example.vet1177.security;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import java.util.List;
+
+/**
+ * Hela säkerhetskonfigurationen för applikationen.
+ *
+ * @EnableConfigurationProperties(JwtProperties.class) — talar om för Spring att
+ * läsa jwt.secret-key och jwt.expiration-ms från application.properties och
+ * skapa en JwtProperties-bean som kan injiceras i JwtService.
+ */
@Configuration
+@EnableConfigurationProperties(JwtProperties.class)
public class SecurityConfig {
+ private final JwtAuthenticationFilter jwtAuthenticationFilter;
+ private final CustomUserDetailsService userDetailsService;
+
+ public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter,
+ CustomUserDetailsService userDetailsService) {
+ this.jwtAuthenticationFilter = jwtAuthenticationFilter;
+ this.userDetailsService = userDetailsService;
+ }
+
+ /**
+ * SecurityFilterChain — definierar hela säkerhetskedjan.
+ *
+ * Varje rad i kedjan lägger till en regel:
+ */
@Bean
- public SecurityFilterChain securityFilterChain(HttpSecurity http)
- throws Exception {
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
+ // CORS — tillåter frontend (annan port/domän) att anropa vårt API.
+ // Utan detta blockerar webbläsaren alla cross-origin requests.
+ .cors(cors -> cors.configurationSource(corsConfigurationSource()))
+
+ // CSRF — avstängt. CSRF-skydd behövs för cookie-baserad auth
+ // men är irrelevant för JWT (token skickas i header, inte cookie).
.csrf(csrf -> csrf.disable())
+
+ // Sessionhantering — STATELESS.
+ // Servern sparar ingen session. All info finns i JWT-tokenen.
+ // Varje request är oberoende — filtret avkodar token varje gång.
+ .sessionManagement(session ->
+ session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ )
+
+ // Endpoint-regler — vilka URLs kräver vad.
+ // Ordningen spelar roll: första matchande regel vinner.
.authorizeHttpRequests(auth -> auth
- .anyRequest().permitAll() // ← temporärt, öppnar allt
+ // Öppna endpoints — ingen token krävs
+ .requestMatchers("/api/auth/**").permitAll()
+ .requestMatchers(HttpMethod.GET, "/api/clinics", "/api/clinics/**").permitAll()
+
+ // Alla andra API-anrop kräver att man är inloggad
+ .anyRequest().authenticated()
)
+
+ // Registrera vårt JWT-filter FÖRE Springs inbyggda UsernamePasswordAuthenticationFilter.
+ // Det betyder att JWT-filtret körs först och sätter SecurityContext
+ // innan Spring kollar om användaren har rätt behörighet.
+ .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
+
.build();
}
+ /**
+ * JwtDecoder-bean — Spring Security använder denna för att verifiera JWT-tokens.
+ * Vi hämtar den från JwtService som redan har konfigurerat den med vår hemliga nyckel.
+ */
+ @Bean
+ public JwtDecoder jwtDecoder(JwtService jwtService) {
+ return jwtService.getJwtDecoder();
+ }
+
+ /**
+ * PasswordEncoder — BCrypt är industristandard.
+ * Används vid registrering (hasha lösenord) och login (verifiera lösenord).
+ * BCrypt saltar automatiskt — samma lösenord ger olika hash varje gång.
+ */
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
-}
\ No newline at end of file
+
+ /**
+ * AuthenticationManager — hanterar login-flödet.
+ *
+ * När Person B:s AuthController anropar authManager.authenticate(email, password):
+ * 1. AuthenticationManager delegerar till DaoAuthenticationProvider
+ * 2. DaoAuthenticationProvider anropar CustomUserDetailsService.loadUserByUsername(email)
+ * 3. DaoAuthenticationProvider jämför lösenord med PasswordEncoder.matches()
+ * 4. Om match → returnerar Authentication med User-objektet
+ * 5. Om mismatch → kastar BadCredentialsException
+ */
+ @Bean
+ public AuthenticationManager authenticationManager(
+ AuthenticationConfiguration authConfig) throws Exception {
+ return authConfig.getAuthenticationManager();
+ }
+
+ /**
+ * DaoAuthenticationProvider — kopplar ihop UserDetailsService med PasswordEncoder.
+ * "Dao" = Data Access Object — den hämtar data från vår databas.
+ */
+ @Bean
+ public DaoAuthenticationProvider authenticationProvider() {
+ DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
+ provider.setPasswordEncoder(passwordEncoder());
+ return provider;
+ }
+
+ /**
+ * CORS-konfiguration — vilka origins (domäner) som får anropa vårt API.
+ *
+ * I utveckling: localhost:5173 (Vite/React), localhost:3000 (Next.js)
+ * I produktion: byt till er riktiga frontend-URL.
+ */
+ @Bean
+ public CorsConfigurationSource corsConfigurationSource() {
+ CorsConfiguration config = new CorsConfiguration();
+ config.setAllowedOrigins(List.of("http://localhost:5173", "http://localhost:3000"));
+ config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
+ config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
+ config.setAllowCredentials(true);
+
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/api/**", config);
+ return source;
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 87967623..b926dda2 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -27,4 +27,8 @@ aws.s3.bucket-name=${S3_BUCKET:}
aws.s3.region=${S3_REGION:}
spring.servlet.multipart.max-file-size=10MB
-spring.servlet.multipart.max-request-size=10MB
\ No newline at end of file
+spring.servlet.multipart.max-request-size=10MB
+
+# JWT Configuration
+jwt.secret-key=${JWT_SECRET:default-dev-secret-key-change-in-production-min-32-chars!!}
+jwt.expiration-ms=86400000
\ No newline at end of file
diff --git a/src/test/java/org/example/vet1177/controller/ActivityLogControllerTest.java b/src/test/java/org/example/vet1177/controller/ActivityLogControllerTest.java
index 00b584a3..aa92f78b 100644
--- a/src/test/java/org/example/vet1177/controller/ActivityLogControllerTest.java
+++ b/src/test/java/org/example/vet1177/controller/ActivityLogControllerTest.java
@@ -1,15 +1,15 @@
package org.example.vet1177.controller;
import org.example.vet1177.entities.*;
+import org.example.vet1177.security.CustomUserDetailsService;
+import org.example.vet1177.security.JwtService;
+import org.example.vet1177.security.SecurityConfig;
import org.example.vet1177.services.ActivityLogService;
-import org.example.vet1177.services.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
-
-// ✅ RÄTT (Boot 4)
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
-import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
-
+import org.springframework.context.annotation.Import;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
@@ -18,11 +18,12 @@
import java.util.UUID;
import static org.mockito.Mockito.*;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(ActivityLogController.class)
-@AutoConfigureMockMvc(addFilters = false) // 🔥 stänger av security (fixar 401)
+@Import(SecurityConfig.class)
class ActivityLogControllerTest {
@Autowired
@@ -32,7 +33,10 @@ class ActivityLogControllerTest {
private ActivityLogService activityLogService;
@MockitoBean
- private UserService userService;
+ private JwtService jwtService;
+
+ @MockitoBean
+ private CustomUserDetailsService customUserDetailsService;
@Test
void shouldReturnLogsForRecord() throws Exception {
@@ -47,6 +51,7 @@ void shouldReturnLogsForRecord() throws Exception {
User user = mock(User.class);
when(user.getId()).thenReturn(userId);
when(user.getName()).thenReturn("Alice");
+ when(user.getAuthorities()).thenReturn(List.of());
// Mock MedicalRecord
MedicalRecord record = mock(MedicalRecord.class);
@@ -61,17 +66,18 @@ void shouldReturnLogsForRecord() throws Exception {
when(log.getMedicalRecord()).thenReturn(record);
when(log.getCreatedAt()).thenReturn(createdAt);
- // Mock service responses
- when(userService.getUserEntityById(userId)).thenReturn(user);
+ // Mock service
when(activityLogService.getByRecord(recordId, user))
.thenReturn(List.of(log));
// Act + Assert
mockMvc.perform(get("/api/activity-logs/record/" + recordId)
- .header("userId", userId.toString()))
+ .with(authentication(new UsernamePasswordAuthenticationToken(
+ user, null, user.getAuthorities()
+ ))))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].description").value("Created case"))
.andExpect(jsonPath("$[0].action").value("CASE_CREATED"))
.andExpect(jsonPath("$[0].performedByName").value("Alice"));
}
-}
\ No newline at end of file
+}
diff --git a/src/test/java/org/example/vet1177/controller/AttachmentControllerTest.java b/src/test/java/org/example/vet1177/controller/AttachmentControllerTest.java
index 01467030..9fb12fa5 100644
--- a/src/test/java/org/example/vet1177/controller/AttachmentControllerTest.java
+++ b/src/test/java/org/example/vet1177/controller/AttachmentControllerTest.java
@@ -5,6 +5,8 @@
import org.example.vet1177.entities.User;
import org.example.vet1177.exception.ForbiddenException;
import org.example.vet1177.exception.ResourceNotFoundException;
+import org.example.vet1177.security.CustomUserDetailsService;
+import org.example.vet1177.security.JwtService;
import org.example.vet1177.security.SecurityConfig;
import org.example.vet1177.services.AttachmentService;
import org.junit.jupiter.api.BeforeEach;
@@ -39,6 +41,12 @@ class AttachmentControllerTest {
@MockitoBean
private AttachmentService attachmentService;
+ @MockitoBean
+ private JwtService jwtService;
+
+ @MockitoBean
+ private CustomUserDetailsService customUserDetailsService;
+
private User vetUser;
private UUID recordId;
private UUID attachmentId;
diff --git a/src/test/java/org/example/vet1177/controller/ClinicControllerTest.java b/src/test/java/org/example/vet1177/controller/ClinicControllerTest.java
index fe8892f1..c0548091 100644
--- a/src/test/java/org/example/vet1177/controller/ClinicControllerTest.java
+++ b/src/test/java/org/example/vet1177/controller/ClinicControllerTest.java
@@ -1,6 +1,8 @@
package org.example.vet1177.controller;
import org.example.vet1177.entities.Clinic;
+import org.example.vet1177.security.CustomUserDetailsService;
+import org.example.vet1177.security.JwtService;
import org.example.vet1177.services.ClinicService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -26,6 +28,12 @@ class ClinicControllerTest {
@MockitoBean
private ClinicService clinicService;
+ @MockitoBean
+ private JwtService jwtService;
+
+ @MockitoBean
+ private CustomUserDetailsService customUserDetailsService;
+
@Test
void shouldCreateClinic() throws Exception {
// Arrange
diff --git a/src/test/java/org/example/vet1177/controller/CommentControllerTest.java b/src/test/java/org/example/vet1177/controller/CommentControllerTest.java
index 37ccdf8b..fe30a6af 100644
--- a/src/test/java/org/example/vet1177/controller/CommentControllerTest.java
+++ b/src/test/java/org/example/vet1177/controller/CommentControllerTest.java
@@ -15,6 +15,8 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.context.annotation.Import;
+import org.example.vet1177.security.CustomUserDetailsService;
+import org.example.vet1177.security.JwtService;
import org.example.vet1177.security.SecurityConfig;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -45,6 +47,12 @@ class CommentControllerTest {
@MockitoBean
private CommentService commentService;
+ @MockitoBean
+ private JwtService jwtService;
+
+ @MockitoBean
+ private CustomUserDetailsService customUserDetailsService;
+
private User currentUser;
private MedicalRecord record;
private Comment comment;
diff --git a/src/test/java/org/example/vet1177/controller/MedicalRecordControllerTest.java b/src/test/java/org/example/vet1177/controller/MedicalRecordControllerTest.java
index 2b7d8a14..38b44129 100644
--- a/src/test/java/org/example/vet1177/controller/MedicalRecordControllerTest.java
+++ b/src/test/java/org/example/vet1177/controller/MedicalRecordControllerTest.java
@@ -15,6 +15,8 @@
import org.example.vet1177.exception.ForbiddenException;
import org.example.vet1177.exception.ResourceNotFoundException;
import org.example.vet1177.policy.MedicalRecordPolicy;
+import org.example.vet1177.security.CustomUserDetailsService;
+import org.example.vet1177.security.JwtService;
import org.example.vet1177.security.SecurityConfig;
import org.example.vet1177.services.MedicalRecordService;
import org.example.vet1177.services.UserService;
@@ -59,6 +61,12 @@ class MedicalRecordControllerTest {
@MockitoBean
private UserService userService;
+ @MockitoBean
+ private JwtService jwtService;
+
+ @MockitoBean
+ private CustomUserDetailsService customUserDetailsService;
+
private User vetUser;
private User ownerUser;
private Pet pet;
diff --git a/src/test/java/org/example/vet1177/controller/VetControllerTest.java b/src/test/java/org/example/vet1177/controller/VetControllerTest.java
index 2ffaa8bc..dc1387ce 100644
--- a/src/test/java/org/example/vet1177/controller/VetControllerTest.java
+++ b/src/test/java/org/example/vet1177/controller/VetControllerTest.java
@@ -6,6 +6,8 @@
import org.example.vet1177.entities.User;
import org.example.vet1177.exception.ForbiddenException;
import org.example.vet1177.exception.ResourceNotFoundException;
+import org.example.vet1177.security.CustomUserDetailsService;
+import org.example.vet1177.security.JwtService;
import org.example.vet1177.policy.AdminPolicy;
import org.example.vet1177.services.VetService;
import org.junit.jupiter.api.Test;
@@ -39,6 +41,12 @@ class VetControllerTest {
@MockitoBean
private AdminPolicy adminPolicy;
+ @MockitoBean
+ private JwtService jwtService;
+
+ @MockitoBean
+ private CustomUserDetailsService customUserDetailsService;
+
// =========================
// POST /api/vets
// =========================
diff --git a/src/test/java/org/example/vet1177/integration/activitylog/ActivityLogIntegrationTest.java b/src/test/java/org/example/vet1177/integration/activitylog/ActivityLogIntegrationTest.java
index 385562fd..7eeb8858 100644
--- a/src/test/java/org/example/vet1177/integration/activitylog/ActivityLogIntegrationTest.java
+++ b/src/test/java/org/example/vet1177/integration/activitylog/ActivityLogIntegrationTest.java
@@ -1,6 +1,5 @@
package org.example.vet1177.integration.activitylog;
-import org.checkerframework.checker.units.qual.C;
import org.example.vet1177.config.AwsS3Properties;
import org.example.vet1177.entities.*;
import org.example.vet1177.integration.TestDataFactory;
@@ -10,15 +9,15 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
-import java.math.BigDecimal;
-import java.time.LocalDate;
import java.util.UUID;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -137,7 +136,9 @@ void should_return_logs_for_owner_only() throws Exception {
// Act & Assert
mockMvc.perform(get("/api/activity-logs/record/" + record.getId())
- .header("userId", owner.getId().toString()))
+ .with(authentication(new UsernamePasswordAuthenticationToken(
+ owner, null, owner.getAuthorities()
+ ))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(2));
}
@@ -166,7 +167,9 @@ void should_allow_vet_in_same_clinic_to_see_logs() throws Exception {
ActivityType.CASE_CREATED, "log");
mockMvc.perform(get("/api/activity-logs/record/" + record.getId())
- .header("userId", vet.getId().toString()))
+ .with(authentication(new UsernamePasswordAuthenticationToken(
+ vet, null, vet.getAuthorities()
+ ))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(1));
}
@@ -197,15 +200,11 @@ void should_filter_out_logs_for_vet_in_other_clinic() throws Exception {
ActivityType.CASE_CREATED, "log");
mockMvc.perform(get("/api/activity-logs/record/" + record.getId())
- .header("userId", vetOtherClinic.getId().toString()))
+ .with(authentication(new UsernamePasswordAuthenticationToken(
+ vetOtherClinic, null, vetOtherClinic.getAuthorities()
+ ))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(0));
}
- @Test
- void should_return_400_if_userId_missing() throws Exception {
-
- mockMvc.perform(get("/api/activity-logs/record/" + UUID.randomUUID()))
- .andExpect(status().isBadRequest());
- }
}
diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties
index 88e85bcc..0fcfdc88 100644
--- a/src/test/resources/application-test.properties
+++ b/src/test/resources/application-test.properties
@@ -1,9 +1,8 @@
# ============================================
-# TEST-profil ? krs av @ActiveProfiles("test")
-# Anvnds av integrationstester med @SpringBootTest
+
# ============================================
-# Separat testdatabas ? aldrig dev-databasen
+
spring.datasource.url=jdbc:h2:mem:testdb;MODE=PostgreSQL
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
@@ -13,14 +12,14 @@ spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.datasource.hikari.driver-class-name=org.h2.Driver
-# Skapa schema frn scratch och rensa efter tester
+
spring.jpa.hibernate.ddl-auto=create-drop
# Hibernate creates schema automatically (schema.sql is NOT used in tests)
spring.sql.init.mode=never
spring.sql.init.data-locations=
-# Stng av Docker Compose i tester
+
# CI hanterar databasen som service container
spring.docker.compose.lifecycle-management=none
@@ -31,8 +30,8 @@ aws.s3.secret-key=minioadmin
aws.s3.bucket-name=vet1177-test-attachments
aws.s3.region=eu-north-1
-# JWT ? dummyvrden fr tester
-jwt.secret=test-secret-key-minst-32-tecken-lang-for-tester
+
+jwt.secret-key=test-secret-key-minst-32-tecken-lang-for-tester
jwt.expiration-ms=86400000
# Mindre output i tester