diff --git a/backend/app/auth.py b/backend/app/auth.py index 4fbe17b..15905e0 100644 --- a/backend/app/auth.py +++ b/backend/app/auth.py @@ -13,7 +13,7 @@ from .config import settings SECRET_KEY = settings.secret_key ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 60 +ACCESS_TOKEN_EXPIRE_MINUTES = settings.access_token_expire_minuted pwd_context = CryptContext(schemes=["bcrypt_sha256"], deprecated="auto") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token") diff --git a/backend/app/config.py b/backend/app/config.py index 17bf487..e530c49 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -14,6 +14,8 @@ class Settings(BaseModel): secret_key: str = os.getenv("SECRET_KEY", "CHANGE_ME") + access_token_expire_minuted: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60")) + @property def database_url(self): return ( diff --git a/backend/app/main.py b/backend/app/main.py index 4159e8d..d5b64ec 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -10,12 +10,24 @@ from .auth import ( create_access_token, get_password_hash, ) from fastapi.security import OAuth2PasswordRequestForm +from fastapi.middleware.cors import CORSMiddleware app = FastAPI(title="Server Worklog") +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # oder deine echte Domain + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) # Nur für lokale Entwicklung, in Produktion via Alembic Base.metadata.create_all(bind=engine) +@app.options("/{path:path}") +def preflight_handler(path: str): + return {} + @app.post("/auth/register", response_model=schemas.UserRead) def register_user(user_in: schemas.UserCreate, db: Session = Depends(get_db)): existing = db.query(models.User).filter(models.User.username == user_in.username).first() @@ -86,6 +98,25 @@ def create_server( db.refresh(server) return server +@app.put("/servers/{server_id}", response_model=schemas.ServerRead) +def update_server( + server_id: int, + server: schemas.ServerUpdate, + db: Session = Depends(get_db), + user=Depends(get_current_user), +): + db_server = db.query(models.Server).filter(models.Server.id == server_id).first() + if not db_server: + raise HTTPException(status_code=404, detail="Server not found") + + db_server.hostname = server.hostname + db_server.description = server.description + db_server.owner_id = server.owner_id + + db.commit() + db.refresh(db_server) + return db_server + @app.get("/servers", response_model=List[schemas.ServerRead]) def list_servers( db: Session = Depends(get_db), diff --git a/backend/app/schemas.py b/backend/app/schemas.py index ec80298..2d88580 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -21,6 +21,9 @@ class ServerBase(BaseModel): class ServerCreate(ServerBase): pass +class ServerUpdate(ServerBase): + pass + class ServerRead(ServerBase): id: int owner: OwnerRead | None = None diff --git a/frontend/app/app.vue b/frontend/app/app.vue index ea4ef5c..54b6347 100644 --- a/frontend/app/app.vue +++ b/frontend/app/app.vue @@ -1,3 +1,18 @@ + + + diff --git a/frontend/app/composables/useAPI.ts b/frontend/app/composables/useAPI.ts new file mode 100644 index 0000000..cb93681 --- /dev/null +++ b/frontend/app/composables/useAPI.ts @@ -0,0 +1,59 @@ +export function useApi() { + const config = useRuntimeConfig() + const { token, logout } = useAuth() + + async function apiFetch(url: string, options: any = {}) { + try { + return await $fetch(url, { + baseURL: config.public.apiBase, + headers: { + ...(options.headers || {}), + ...(token.value ? { Authorization: `Bearer ${token.value}` } : {}) + }, + ...options + }) + } catch (error: any) { + // Netzwerkfehler oder Server nicht erreichbar + if (!error.response) { + console.error("API unreachable:", error) + throw new Error("Der Server ist nicht erreichbar.") + } + + const status = error.response.status + const data = error.response._data + + // 401 → Token ungültig oder abgelaufen + if (status === 401) { + console.warn("API returned 401 — logging out") + logout() + return { unauthorized: true } + } + + // 403 → keine Berechtigung + if (status === 403) { + throw new Error("Du hast keine Berechtigung für diese Aktion.") + } + + // 400 → Validierungsfehler + if (status === 400) { + throw new Error(data?.detail || "Ungültige Eingabe.") + } + + // 404 → nicht gefunden + if (status === 404) { + throw new Error("Die angeforderte Ressource wurde nicht gefunden.") + } + + // 500 → Serverfehler + if (status >= 500) { + console.error("Server error:", data) + throw new Error("Interner Serverfehler. Bitte später erneut versuchen.") + } + + // Fallback + throw new Error(data?.detail || "Unbekannter Fehler.") + } + } + + return { apiFetch } +} diff --git a/frontend/app/composables/useAuth.ts b/frontend/app/composables/useAuth.ts index 3acb2c5..15e3cd2 100644 --- a/frontend/app/composables/useAuth.ts +++ b/frontend/app/composables/useAuth.ts @@ -1,15 +1,46 @@ +function decodeJwt(token: string) { + try { + const payload = token.split(".")[1] + const decoded = JSON.parse(atob(payload)) + return decoded + } catch { + return null + } +} export function useAuth() { - const token = useState("token", () => null) + const token = useState("token", () => null) function setToken(t: string) { token.value = t - localStorage.setItem("token", t) + + if (typeof window !== "undefined") { + localStorage.setItem("token", t) + } } function loadToken() { - token.value = localStorage.getItem("token") + if (typeof window !== "undefined") { + const stored = localStorage.getItem("token") + if (!stored) return + + const payload = decodeJwt(stored) + if( !payload || !payload.exp || payload.exp * 1000 < Date.now()) { + console.info("Token expired - removing it from storage") + localStorage.removeItem("token") + token.value = null + } + token.value = stored + + } } - return { token, setToken, loadToken } -} + function logout() { + token.value = null + if (typeof window !== "undefined") { + localStorage.removeItem("token") + } + } + + return { token, setToken, loadToken, logout } +} diff --git a/frontend/app/composables/useOwners.ts b/frontend/app/composables/useOwners.ts new file mode 100644 index 0000000..6af05a0 --- /dev/null +++ b/frontend/app/composables/useOwners.ts @@ -0,0 +1,33 @@ +import { ref } from "vue" +import { isUnauthorized } from "~/utils/auth" + +export function useOwners() { + const owners = ref([]) + const loading = ref(false) + const error = ref("") + + const { apiFetch } = useApi() + + async function loadOwners() { + loading.value = true + error.value = "" + + const result = await apiFetch("/owners") + + if (isUnauthorized(result)) { + loading.value = false + return result + } + + owners.value = result + loading.value = false + return result + } + + return { + owners, + loading, + error, + loadOwners + } +} diff --git a/frontend/app/composables/useServers.ts b/frontend/app/composables/useServers.ts new file mode 100644 index 0000000..63408e9 --- /dev/null +++ b/frontend/app/composables/useServers.ts @@ -0,0 +1,33 @@ +import { ref } from "vue" +import { isUnauthorized } from "~/utils/auth" + +export function useServers() { + const servers = ref([]) + const loading = ref(false) + const error = ref("") + + const { apiFetch } = useApi() + + async function loadServers() { + loading.value = true + error.value = "" + + const result = await apiFetch("/servers") + + if (isUnauthorized(result)) { + loading.value = false + return result + } + + servers.value = result + loading.value = false + return result + } + + return { + servers, + loading, + error, + loadServers + } +} diff --git a/frontend/app/layouts/default.vue b/frontend/app/layouts/default.vue index 1a115bb..54ae58a 100644 --- a/frontend/app/layouts/default.vue +++ b/frontend/app/layouts/default.vue @@ -1,12 +1,48 @@ + + - diff --git a/frontend/app/middleware/auth.global.ts b/frontend/app/middleware/auth.global.ts new file mode 100644 index 0000000..2fcc50d --- /dev/null +++ b/frontend/app/middleware/auth.global.ts @@ -0,0 +1,15 @@ +export default defineNuxtRouteMiddleware((to, from) => { + if (import.meta.server) return // SSR ignorieren + // allow login-page + + if (to.meta.requiresAuth !== true) { + return + } + + const { token, loadToken } = useAuth() + loadToken() + + if (!token.value && to.path !== "/login") { + return navigateTo("/login") + } +}) diff --git a/frontend/app/pages/login.vue b/frontend/app/pages/login.vue new file mode 100644 index 0000000..de69707 --- /dev/null +++ b/frontend/app/pages/login.vue @@ -0,0 +1,64 @@ + + + diff --git a/frontend/app/pages/owners.vue b/frontend/app/pages/owners.vue deleted file mode 100644 index 1a6bf69..0000000 --- a/frontend/app/pages/owners.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - - diff --git a/frontend/app/pages/owners/index.vue b/frontend/app/pages/owners/index.vue new file mode 100644 index 0000000..d03b97c --- /dev/null +++ b/frontend/app/pages/owners/index.vue @@ -0,0 +1,96 @@ + + + diff --git a/frontend/app/pages/servers/index.vue b/frontend/app/pages/servers/index.vue new file mode 100644 index 0000000..f4c51be --- /dev/null +++ b/frontend/app/pages/servers/index.vue @@ -0,0 +1,234 @@ + + +