225 lines
6.6 KiB
TypeScript
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 };
|
|
});
|
|
}
|