221 lines
6.9 KiB
TypeScript
221 lines
6.9 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"
|
|
|
|
export default async function authRoutes(server: FastifyInstance) {
|
|
// Registrierung
|
|
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" },
|
|
},
|
|
},
|
|
response: {
|
|
200: {
|
|
type: "object",
|
|
properties: {
|
|
user: { type: "object" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, async (req, reply) => {
|
|
const body = req.body as { email: string; password: string };
|
|
|
|
if (!body.email || !body.password) {
|
|
return reply.code(400).send({ error: "Email and password required" });
|
|
}
|
|
|
|
// Passwort hashen
|
|
const passwordHash = await bcrypt.hash(body.password, 10);
|
|
|
|
// User speichern
|
|
const { data, error } = await server.supabase
|
|
.from("auth_users")
|
|
.insert({ email: body.email, password_hash: passwordHash })
|
|
.select("id, email")
|
|
.single();
|
|
|
|
if (error) {
|
|
return reply.code(400).send({ error: error.message });
|
|
}
|
|
|
|
return { user: data };
|
|
});
|
|
|
|
// 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" },
|
|
},
|
|
},
|
|
response: {
|
|
200: {
|
|
type: "object",
|
|
properties: {
|
|
token: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, async (req, reply) => {
|
|
const body = req.body as { email: string; password: string };
|
|
|
|
if (!body.email || !body.password) {
|
|
return reply.code(400).send({ error: "Email and password required" });
|
|
}
|
|
|
|
console.log(req.tenant)
|
|
|
|
/**
|
|
* Wenn das Tenant Objekt verfügbar ist, befindet sich das Backend im Single Tenant Modus.
|
|
* Es werden nur Benutzer zugelassen, welche auschließlich diesem Tenant angehören.
|
|
* Das zeigt sich über das im User gesetzte Tenant Feld
|
|
*
|
|
* */
|
|
let user = null
|
|
let error = null
|
|
if(req.tenant) {
|
|
// User finden
|
|
const { data, error } = await server.supabase
|
|
.from("auth_users")
|
|
.select("*, tenants!auth_tenant_users(*)")
|
|
.eq("email", body.email)
|
|
|
|
console.log(data)
|
|
console.log(error)
|
|
|
|
// @ts-ignore
|
|
user = (data || []).find(i => i.tenants.find(x => x.id === req.tenant.id))
|
|
console.log(user)
|
|
if(error) {
|
|
return reply.code(500).send({ error: "Internal Server Error" });
|
|
}
|
|
} else {
|
|
// User finden
|
|
const { data, error } = await server.supabase
|
|
.from("auth_users")
|
|
.select("*")
|
|
.eq("email", body.email)
|
|
.single();
|
|
user = data
|
|
if(error) {
|
|
console.log(error);
|
|
return reply.code(500).send({ error: "Internal Server Error" });
|
|
}
|
|
}
|
|
|
|
if(!user) {
|
|
return reply.code(401).send({ error: "Invalid credentials" });
|
|
} else {
|
|
|
|
console.log(user);
|
|
console.log(body)
|
|
|
|
const valid = await bcrypt.compare(body.password, user.password_hash);
|
|
if (!valid) {
|
|
return reply.code(401).send({ error: "Invalid credentials" });
|
|
} else {
|
|
const token = jwt.sign(
|
|
{ user_id: user.id, email: user.email, tenant_id: req.tenant ? req.tenant.id : null },
|
|
process.env.JWT_SECRET!,
|
|
{ expiresIn: "3h" }
|
|
);
|
|
|
|
reply.setCookie("token", token, {
|
|
path: "/",
|
|
httpOnly: true,
|
|
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
|
|
secure: process.env.NODE_ENV === "production", // lokal: false, prod: true
|
|
maxAge: 60 * 60 * 3, // 3 Stunden
|
|
})
|
|
|
|
return { token };
|
|
}
|
|
}
|
|
});
|
|
|
|
server.post("/auth/logout", {
|
|
schema: {
|
|
tags: ["Auth"],
|
|
summary: "Logout User (löscht Cookie)"
|
|
},
|
|
}, async (req, reply) => {
|
|
reply.clearCookie("token", {
|
|
path: "/",
|
|
httpOnly: true,
|
|
secure: process.env.NODE_ENV === "production",
|
|
sameSite: "lax",
|
|
})
|
|
|
|
return { success: true }
|
|
})
|
|
|
|
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 }
|
|
|
|
// User finden
|
|
const { data: user, error } = await server.supabase
|
|
.from("auth_users")
|
|
.select("id, email")
|
|
.eq("email", email)
|
|
.single()
|
|
|
|
if (error || !user) {
|
|
return reply.code(404).send({ error: "User not found" })
|
|
}
|
|
|
|
// Neues Passwort generieren
|
|
const plainPassword = generateRandomPassword()
|
|
const passwordHash = await hashPassword(plainPassword)
|
|
|
|
// In DB updaten
|
|
const { error: updateError } = await server.supabase
|
|
.from("auth_users")
|
|
.update({ password_hash: passwordHash, must_change_password: true })
|
|
.eq("id", user.id)
|
|
|
|
if (updateError) {
|
|
return reply.code(500).send({ error: "Could not update password" })
|
|
}
|
|
|
|
// Mail verschicken
|
|
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 }
|
|
})
|
|
} |