🚀 Backend komplett implementiert

Features:
- Auth mit JWT + Argon2 (Login, Register, Refresh)
- Rollen-System (Chef/Disponent/Mitarbeiter)
- User Management mit Berechtigungen
- Aufträge mit Zuweisungen
- Verfügbarkeitsplanung
- Stundenzettel mit Foto-Upload Support
- Modulares System mit Config
- Entwickler-Panel Endpoints

Tech:
- Deno + Oak
- PostgreSQL
- CORS enabled
This commit is contained in:
2026-02-20 15:12:06 +00:00
parent a07c2ad858
commit ee19e45171
16 changed files with 2079 additions and 2 deletions

239
src/routes/auth.ts Normal file
View File

@@ -0,0 +1,239 @@
import { Router } from "@oak/oak";
import { query, queryOne, execute } from "../db/postgres.ts";
import {
hashPassword,
verifyPassword,
generateAccessToken,
generateRefreshToken,
generateRandomToken,
verifyToken
} from "../utils/auth.ts";
import { AppError } from "../middleware/error.ts";
import { authMiddleware } from "../middleware/auth.ts";
import type { User } from "../types/index.ts";
export const authRouter = new Router({ prefix: "/api/auth" });
// Register (first user becomes chef, or must be created by higher role)
authRouter.post("/register", async (ctx) => {
const body = await ctx.request.body.json();
const { email, password, first_name, last_name, phone, org_slug } = body;
if (!email || !password || !first_name || !last_name || !org_slug) {
throw new AppError("Missing required fields", 400);
}
// Find organization
const org = await queryOne<{ id: string }>(
"SELECT id FROM organizations WHERE slug = $1",
[org_slug]
);
if (!org) {
throw new AppError("Organization not found", 404);
}
// Check if email already exists
const existing = await queryOne<{ id: string }>(
"SELECT id FROM users WHERE org_id = $1 AND email = $2",
[org.id, email]
);
if (existing) {
throw new AppError("Email already registered", 409);
}
// Check if this is the first user (becomes chef)
const userCount = await queryOne<{ count: string }>(
"SELECT COUNT(*) as count FROM users WHERE org_id = $1",
[org.id]
);
const isFirstUser = parseInt(userCount?.count || "0") === 0;
const role = isFirstUser ? "chef" : "mitarbeiter";
// Hash password and create user
const passwordHash = await hashPassword(password);
const result = await queryOne<{ id: string }>(
`INSERT INTO users (org_id, email, password_hash, role, first_name, last_name, phone, active)
VALUES ($1, $2, $3, $4, $5, $6, $7, true)
RETURNING id`,
[org.id, email, passwordHash, role, first_name, last_name, phone || null]
);
ctx.response.status = 201;
ctx.response.body = {
message: "Registration successful",
userId: result?.id,
role,
isFirstUser
};
});
// Login
authRouter.post("/login", async (ctx) => {
const body = await ctx.request.body.json();
const { email, password, org_slug } = body;
if (!email || !password || !org_slug) {
throw new AppError("Email, password and organization required", 400);
}
// Find user with org
const user = await queryOne<User & { org_id: string }>(
`SELECT u.*, o.slug as org_slug
FROM users u
JOIN organizations o ON u.org_id = o.id
WHERE u.email = $1 AND o.slug = $2 AND u.active = true`,
[email, org_slug]
);
if (!user) {
throw new AppError("Invalid credentials", 401);
}
// Verify password
const valid = await verifyPassword(password, user.password_hash || "");
if (!valid) {
throw new AppError("Invalid credentials", 401);
}
// Generate tokens
const accessToken = await generateAccessToken(user.id, user.org_id, user.role, user.email);
const refreshToken = await generateRefreshToken(user.id);
// Store refresh token hash
const tokenHash = await hashPassword(refreshToken);
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
await execute(
`INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3)`,
[user.id, tokenHash, expiresAt]
);
// Update last login
await execute(
`UPDATE users SET last_login = NOW() WHERE id = $1`,
[user.id]
);
ctx.response.body = {
accessToken,
refreshToken,
user: {
id: user.id,
email: user.email,
role: user.role,
first_name: user.first_name,
last_name: user.last_name,
},
};
});
// Refresh token
authRouter.post("/refresh", async (ctx) => {
const body = await ctx.request.body.json();
const { refreshToken } = body;
if (!refreshToken) {
throw new AppError("Refresh token required", 400);
}
// Verify JWT structure
const payload = await verifyToken(refreshToken);
if (!payload || (payload as unknown as { type?: string }).type !== "refresh") {
throw new AppError("Invalid refresh token", 401);
}
// Get user
const user = await queryOne<User>(
`SELECT * FROM users WHERE id = $1 AND active = true`,
[payload.sub]
);
if (!user) {
throw new AppError("User not found", 404);
}
// Generate new access token
const accessToken = await generateAccessToken(user.id, user.org_id, user.role, user.email);
ctx.response.body = { accessToken };
});
// Logout (invalidate refresh tokens)
authRouter.post("/logout", authMiddleware, async (ctx) => {
const userId = ctx.state.auth.user.id;
await execute(
`DELETE FROM refresh_tokens WHERE user_id = $1`,
[userId]
);
ctx.response.body = { message: "Logged out successfully" };
});
// Get current user
authRouter.get("/me", authMiddleware, async (ctx) => {
const userId = ctx.state.auth.user.id;
const user = await queryOne<User>(
`SELECT id, org_id, email, role, first_name, last_name, phone, avatar_url,
created_at, last_login
FROM users WHERE id = $1`,
[userId]
);
if (!user) {
throw new AppError("User not found", 404);
}
ctx.response.body = { user };
});
// Change password
authRouter.post("/change-password", authMiddleware, async (ctx) => {
const userId = ctx.state.auth.user.id;
const body = await ctx.request.body.json();
const { currentPassword, newPassword } = body;
if (!currentPassword || !newPassword) {
throw new AppError("Current and new password required", 400);
}
if (newPassword.length < 8) {
throw new AppError("Password must be at least 8 characters", 400);
}
// Get current password hash
const user = await queryOne<{ password_hash: string }>(
`SELECT password_hash FROM users WHERE id = $1`,
[userId]
);
if (!user) {
throw new AppError("User not found", 404);
}
// Verify current password
const valid = await verifyPassword(currentPassword, user.password_hash);
if (!valid) {
throw new AppError("Current password is incorrect", 401);
}
// Hash and update new password
const newHash = await hashPassword(newPassword);
await execute(
`UPDATE users SET password_hash = $1 WHERE id = $2`,
[newHash, userId]
);
// Invalidate all refresh tokens
await execute(
`DELETE FROM refresh_tokens WHERE user_id = $1`,
[userId]
);
ctx.response.body = { message: "Password changed successfully" };
});