Files
FEDEO/src/routes/auth/auth.ts
2025-12-07 22:45:02 +01:00

225 lines
6.6 KiB
TypeScript

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";
export default async function authRoutes(server: FastifyInstance) {
// -----------------------------------------------------
// REGISTER
// -----------------------------------------------------
server.post("/auth/register", {
schema: {
tags: ["Auth"],
summary: "Register User",
body: {
type: "object",
required: ["email", "password"],
properties: {
email: { type: "string", format: "email" },
password: { type: "string" },
},
},
},
}, async (req, reply) => {
const body = req.body as { email: string; password: string };
const passwordHash = await bcrypt.hash(body.password, 10);
const [user] = await server.db
.insert(authUsers)
.values({
email: body.email.toLowerCase(),
passwordHash,
})
.returning({
id: authUsers.id,
email: authUsers.email,
});
return { user };
});
// -----------------------------------------------------
// LOGIN
// -----------------------------------------------------
server.post("/auth/login", {
schema: {
tags: ["Auth"],
summary: "Login User",
body: {
type: "object",
required: ["email", "password"],
properties: {
email: { type: "string", format: "email" },
password: { type: "string" },
},
},
},
}, async (req, reply) => {
const body = req.body as { email: string; password: string };
let user: any = null;
// -------------------------------
// SINGLE TENANT MODE
// -------------------------------
/* if (req.tenant) {
const tenantId = req.tenant.id;
const result = await server.db
.select({
user: authUsers,
})
.from(authUsers)
.innerJoin(
authTenantUsers,
eq(authTenantUsers.userId, authUsers.id)
)
.innerJoin(
tenants,
eq(authTenantUsers.tenantId, tenants.id)
)
.where(and(
eq(authUsers.email, body.email.toLowerCase()),
eq(authTenantUsers.tenantId, tenantId)
));
if (result.length === 0) {
return reply.code(401).send({ error: "Invalid credentials" });
}
user = result[0].user;
// -------------------------------
// MULTI TENANT MODE
// -------------------------------
} else {*/
const [found] = await server.db
.select()
.from(authUsers)
.where(eq(authUsers.email, body.email.toLowerCase()))
.limit(1);
if (!found) {
return reply.code(401).send({ error: "Invalid credentials" });
}
user = found;
/*}*/
// Passwort prüfen
const valid = await bcrypt.compare(body.password, user.passwordHash);
if (!valid) {
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" }
);
reply.setCookie("token", token, {
path: "/",
httpOnly: true,
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 3,
});
return { token };
});
// -----------------------------------------------------
// LOGOUT
// -----------------------------------------------------
server.post("/auth/logout", {
schema: {
tags: ["Auth"],
summary: "Logout User"
}
}, async (req, reply) => {
reply.clearCookie("token", {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
});
return { success: true };
});
// -----------------------------------------------------
// PASSWORD RESET
// -----------------------------------------------------
server.post("/auth/password/reset", {
schema: {
tags: ["Auth"],
summary: "Reset Password",
body: {
type: "object",
required: ["email"],
properties: {
email: { type: "string", format: "email" }
}
}
}
}, async (req, reply) => {
const { email } = req.body as { email: string };
const [user] = await server.db
.select({
id: authUsers.id,
email: authUsers.email,
})
.from(authUsers)
.where(eq(authUsers.email, email.toLowerCase()))
.limit(1);
if (!user) {
return reply.code(404).send({ error: "User not found" });
}
const plainPassword = generateRandomPassword();
const passwordHash = await hashPassword(plainPassword);
await server.db
.update(authUsers)
.set({
passwordHash,
// @ts-ignore
mustChangePassword: true,
})
.where(eq(authUsers.id, user.id));
await sendMail(
user.email,
"FEDEO | Dein neues Passwort",
`
<p>Hallo,</p>
<p>Dein Passwort wurde zurückgesetzt.</p>
<p><strong>Neues Passwort:</strong> ${plainPassword}</p>
<p>Bitte ändere es nach dem Login umgehend.</p>
`
);
return { success: true };
});
}