KI-AGENT: Persistente App-Sessions mit Refresh-Tokens ergänzen

This commit is contained in:
2026-07-20 22:01:08 +02:00
parent d8273e794d
commit 657a4f9e85
11 changed files with 405 additions and 60 deletions

View File

@@ -1,14 +1,18 @@
import { FastifyInstance } from "fastify";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { generateRandomPassword, hashPassword } from "../../utils/password";
import { sendMail } from "../../utils/mailer";
import { secrets } from "../../utils/secrets";
import { authUsers } from "../../../db/schema";
import { authTenantUsers } from "../../../db/schema";
import { tenants } from "../../../db/schema";
import { eq, and } from "drizzle-orm";
import { eq } from "drizzle-orm";
import {
createAccessToken,
issueRefreshToken,
revokeRefreshToken,
rotateRefreshToken,
} from "../../utils/authTokens";
export default async function authRoutes(server: FastifyInstance) {
@@ -122,15 +126,9 @@ export default async function authRoutes(server: FastifyInstance) {
return reply.code(401).send({ error: "Invalid credentials" });
}
const token = jwt.sign(
{
user_id: user.id,
email: user.email,
tenant_id: req.tenant?.id ?? null,
},
secrets.JWT_SECRET!,
{ expiresIn: "6h" }
);
const tenantId = req.tenant?.id ? Number(req.tenant.id) : null;
const token = createAccessToken(user, tenantId);
const refreshToken = await issueRefreshToken(server, user.id, tenantId);
reply.setCookie("token", token, {
path: "/",
@@ -140,9 +138,31 @@ export default async function authRoutes(server: FastifyInstance) {
maxAge: 60 * 60 * 6,
});
return { token };
return { token, refreshToken };
});
server.post("/auth/refresh", {
schema: {
tags: ["Auth"],
summary: "Refresh a persistent session",
body: {
type: "object",
required: ["refreshToken"],
properties: {
refreshToken: { type: "string" },
},
},
},
}, async (req, reply) => {
const { refreshToken } = req.body as { refreshToken: string };
const refreshed = await rotateRefreshToken(server, refreshToken);
if (!refreshed) {
return reply.code(401).send({ error: "Invalid or expired refresh token" });
}
return refreshed;
});
// -----------------------------------------------------
// LOGOUT
@@ -153,6 +173,10 @@ export default async function authRoutes(server: FastifyInstance) {
summary: "Logout User"
}
}, async (req, reply) => {
const refreshToken = (req.body as { refreshToken?: string } | undefined)?.refreshToken;
if (refreshToken) {
await revokeRefreshToken(server, refreshToken);
}
reply.clearCookie("token", {
path: "/",
httpOnly: true,

View File

@@ -14,6 +14,7 @@ import {
import {and, desc, eq, inArray} from "drizzle-orm"
import { enrichProfilesWithBranches } from "../utils/profileBranches"
import { enrichProfilesWithTeams } from "../utils/profileTeams"
import { rotateRefreshToken } from "../utils/authTokens"
export default async function tenantRoutes(server: FastifyInstance) {
@@ -65,7 +66,7 @@ export default async function tenantRoutes(server: FastifyInstance) {
return reply.code(401).send({ error: "Unauthorized" })
}
const { tenant_id } = req.body as { tenant_id: string }
const { tenant_id, refreshToken } = req.body as { tenant_id: string; refreshToken?: string }
if (!tenant_id) return reply.code(400).send({ error: "tenant_id required" })
// prüfen ob der User zu diesem Tenant gehört
@@ -81,7 +82,22 @@ export default async function tenantRoutes(server: FastifyInstance) {
return reply.code(403).send({ error: "Not a member of this tenant" })
}
// JWT neu erzeugen
if (refreshToken) {
const session = await rotateRefreshToken(
server,
refreshToken,
Number(tenant_id),
req.user.user_id
)
if (!session) {
return reply.code(401).send({ error: "Invalid or expired refresh token" })
}
return session
}
// Abwärtskompatibilität für Clients ohne persistente Session
const token = jwt.sign(
{
user_id: req.user.user_id,