This commit is contained in:
2025-09-12 18:29:13 +02:00
parent dc385b8422
commit c98394b5bf
17 changed files with 1786 additions and 9 deletions

View File

@@ -0,0 +1,76 @@
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 authRoutesAuthenticated(server: FastifyInstance) {
server.post("/auth/password/change", {
schema: {
tags: ["Auth"],
summary: "Reset Password after forced change",
body: {
type: "object",
required: ["old_password", "new_password"],
properties: {
old_password: { type: "string" },
new_password: { type: "string" },
},
},
response: {
200: {
type: "object",
properties: {
success: { type: "boolean" },
},
},
},
},
}, async (req, reply) => {
const { old_password, new_password } = req.body as { old_password: string; new_password: string };
console.log(req.user)
const user_id = req.user?.user_id; // kommt aus JWT Middleware
if (!user_id) {
return reply.code(401).send({ error: "Unauthorized" });
}
// Nutzer laden
const { data: user, error } = await server.supabase
.from("auth_users")
.select("id, password_hash, must_change_password")
.eq("id", user_id)
.single();
if (error || !user) {
return reply.code(404).send({ error: "User not found" });
}
// Altes Passwort prüfen
const valid = await bcrypt.compare(old_password, user.password_hash);
if (!valid) {
return reply.code(401).send({ error: "Old password incorrect" });
}
// Neues Passwort hashen
const newHash = await bcrypt.hash(new_password, 10);
// Speichern + Flag zurücksetzen
const { error: updateError } = await server.supabase
.from("auth_users")
.update({
password_hash: newHash,
must_change_password: false,
updated_at: new Date().toISOString(),
})
.eq("id", user_id);
if (updateError) {
console.log(updateError);
return reply.code(500).send({ error: "Password update failed" });
}
return { success: true };
});
}

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

@@ -0,0 +1,221 @@
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 }
})
}

80
src/routes/auth/me.ts Normal file
View File

@@ -0,0 +1,80 @@
import { FastifyInstance } from "fastify";
export default async function meRoutes(server: FastifyInstance) {
server.get("/me", async (req, reply) => {
const authUser = req.user // kommt aus JWT (user_id + tenant_id)
if (!authUser) {
return reply.code(401).send({ error: "Unauthorized" })
}
const user_id = req.user.user_id
const tenant_id = req.user.tenant_id
// 1. User laden
const { data: user, error: userError } = await server.supabase
.from("auth_users")
.select("id, email, created_at, must_change_password")
.eq("id", authUser.user_id)
.single()
if (userError || !user) {
return reply.code(401).send({ error: "User not found" })
}
// 2. Tenants laden (alle Tenants des Users)
const { data: tenantLinks, error: tenantLinksError } = await server.supabase
.from("auth_users")
.select(`*, tenants!auth_tenant_users ( id, name,short, locked, extraModules, businessInfo, numberRanges, dokuboxkey )`)
.eq("id", authUser.user_id)
.single();
if (tenantLinksError) {
console.log(tenantLinksError)
return reply.code(401).send({ error: "Tenant Error" })
}
const tenants = tenantLinks?.tenants
// 3. Aktiven Tenant bestimmen
const activeTenant = authUser.tenant_id /*|| tenants[0].id*/
// 4. Profil für den aktiven Tenant laden
let profile = null
if (activeTenant) {
const { data: profileData } = await server.supabase
.from("auth_profiles")
.select("*")
.eq("user_id", user.id)
.eq("tenant_id", activeTenant)
.single()
profile = profileData
}
// 5. Permissions laden (über Funktion)
const { data: permissionsData, error: permissionsError } = await server.supabase
.rpc("auth_get_user_permissions", {
uid: user.id,
tid: activeTenant || null
})
if(permissionsError) {
console.log(permissionsError)
}
console.log(permissionsData)
const permissions = permissionsData.map(i => i.permission) || []
// 6. Response zurückgeben
return {
user,
tenants,
activeTenant,
profile,
permissions
}
})
}

67
src/routes/auth/user.ts Normal file
View File

@@ -0,0 +1,67 @@
import { FastifyInstance } from "fastify";
export default async function userRoutes(server: FastifyInstance) {
//TODO: PERMISSIONS Rückmeldung beschränken
server.get("/user/:id", async (req, reply) => {
const authUser = req.user // kommt aus JWT (user_id + tenant_id)
const {id} = req.params
if (!authUser) {
return reply.code(401).send({ error: "Unauthorized" })
}
// 1. User laden
const { data: user, error: userError } = await server.supabase
.from("auth_users")
.select("id, email, created_at, must_change_password")
.eq("id", id)
.single()
if (userError || !user) {
return reply.code(401).send({ error: "User not found" })
}
// 2. Tenants laden (alle Tenants des Users)
/*const { data: tenantLinks, error: tenantLinksError } = await server.supabase
.from("auth_users")
.select(`*, tenants!auth_tenant_users ( id, name, locked )`)
.eq("id", authUser.user_id)
.single();
if (tenantLinksError) {
console.log(tenantLinksError)
return reply.code(401).send({ error: "Tenant Error" })
}
const tenants = tenantLinks?.tenants*/
// 3. Aktiven Tenant bestimmen
const activeTenant = authUser.tenant_id /*|| tenants[0].id*/
// 4. Profil für den aktiven Tenant laden
let profile = null
if (activeTenant) {
const { data: profileData } = await server.supabase
.from("auth_profiles")
.select("*")
.eq("user_id", id)
.eq("tenant_id", activeTenant)
.single()
profile = profileData
}
// 5. Permissions laden (über Funktion)
// 6. Response zurückgeben
return {
user,
profile,
}
})
}