Added Backend
This commit is contained in:
96
backend/src/routes/auth/auth-authenticated.ts
Normal file
96
backend/src/routes/auth/auth-authenticated.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import bcrypt from "bcrypt"
|
||||
import { eq } from "drizzle-orm"
|
||||
|
||||
import { authUsers } from "../../../db/schema" // wichtig: Drizzle Schema importieren!
|
||||
|
||||
export default async function authRoutesAuthenticated(server: FastifyInstance) {
|
||||
|
||||
server.post("/auth/password/change", {
|
||||
schema: {
|
||||
tags: ["Auth"],
|
||||
summary: "Change password (after login or forced reset)",
|
||||
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) => {
|
||||
|
||||
try {
|
||||
const { old_password, new_password } = req.body as {
|
||||
old_password: string
|
||||
new_password: string
|
||||
}
|
||||
|
||||
const userId = req.user?.user_id
|
||||
if (!userId) {
|
||||
//@ts-ignore
|
||||
return reply.code(401).send({ error: "Unauthorized" })
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 1) User laden
|
||||
// -----------------------------------------------------
|
||||
const [user] = await server.db
|
||||
.select({
|
||||
id: authUsers.id,
|
||||
passwordHash: authUsers.passwordHash,
|
||||
mustChangePassword: authUsers.must_change_password
|
||||
})
|
||||
.from(authUsers)
|
||||
.where(eq(authUsers.id, userId))
|
||||
.limit(1)
|
||||
|
||||
if (!user) {
|
||||
//@ts-ignore
|
||||
return reply.code(404).send({ error: "User not found" })
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 2) Altes PW prüfen
|
||||
// -----------------------------------------------------
|
||||
const valid = await bcrypt.compare(old_password, user.passwordHash)
|
||||
if (!valid) {
|
||||
//@ts-ignore
|
||||
return reply.code(401).send({ error: "Old password incorrect" })
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 3) Neues PW hashen
|
||||
// -----------------------------------------------------
|
||||
const newHash = await bcrypt.hash(new_password, 10)
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 4) Updaten
|
||||
// -----------------------------------------------------
|
||||
await server.db
|
||||
.update(authUsers)
|
||||
.set({
|
||||
passwordHash: newHash,
|
||||
must_change_password: false,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(authUsers.id, userId))
|
||||
|
||||
return { success: true }
|
||||
|
||||
} catch (err) {
|
||||
console.error("POST /auth/password/change ERROR:", err)
|
||||
//@ts-ignore
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
}
|
||||
})
|
||||
}
|
||||
224
backend/src/routes/auth/auth.ts
Normal file
224
backend/src/routes/auth/auth.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
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 };
|
||||
});
|
||||
}
|
||||
140
backend/src/routes/auth/me.ts
Normal file
140
backend/src/routes/auth/me.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import {
|
||||
authUsers,
|
||||
authTenantUsers,
|
||||
tenants,
|
||||
authProfiles,
|
||||
authUserRoles,
|
||||
authRoles,
|
||||
authRolePermissions,
|
||||
} from "../../../db/schema"
|
||||
import { eq, and, or, isNull } from "drizzle-orm"
|
||||
|
||||
export default async function meRoutes(server: FastifyInstance) {
|
||||
server.get("/me", async (req, reply) => {
|
||||
try {
|
||||
const authUser = req.user
|
||||
|
||||
if (!authUser) {
|
||||
return reply.code(401).send({ error: "Unauthorized" })
|
||||
}
|
||||
|
||||
const userId = authUser.user_id
|
||||
const activeTenantId = authUser.tenant_id
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 1) USER LADEN
|
||||
// ----------------------------------------------------
|
||||
const userResult = await server.db
|
||||
.select({
|
||||
id: authUsers.id,
|
||||
email: authUsers.email,
|
||||
created_at: authUsers.created_at,
|
||||
must_change_password: authUsers.must_change_password,
|
||||
})
|
||||
.from(authUsers)
|
||||
.where(eq(authUsers.id, userId))
|
||||
.limit(1)
|
||||
|
||||
const user = userResult[0]
|
||||
|
||||
if (!user) {
|
||||
return reply.code(401).send({ error: "User not found" })
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 2) TENANTS LADEN
|
||||
// ----------------------------------------------------
|
||||
const tenantRows = await server.db
|
||||
.select({
|
||||
id: tenants.id,
|
||||
name: tenants.name,
|
||||
short: tenants.short,
|
||||
locked: tenants.locked,
|
||||
extraModules: tenants.extraModules,
|
||||
businessInfo: tenants.businessInfo,
|
||||
numberRanges: tenants.numberRanges,
|
||||
dokuboxkey: tenants.dokuboxkey,
|
||||
standardEmailForInvoices: tenants.standardEmailForInvoices,
|
||||
standardPaymentDays: tenants.standardPaymentDays,
|
||||
})
|
||||
.from(authTenantUsers)
|
||||
.innerJoin(tenants, eq(authTenantUsers.tenant_id, tenants.id))
|
||||
.where(eq(authTenantUsers.user_id, userId))
|
||||
|
||||
const tenantList = tenantRows ?? []
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 3) ACTIVE TENANT
|
||||
// ----------------------------------------------------
|
||||
const activeTenant = activeTenantId
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 4) PROFIL LADEN
|
||||
// ----------------------------------------------------
|
||||
let profile = null
|
||||
if (activeTenantId) {
|
||||
const profileResult = await server.db
|
||||
.select()
|
||||
.from(authProfiles)
|
||||
.where(
|
||||
and(
|
||||
eq(authProfiles.user_id, userId),
|
||||
eq(authProfiles.tenant_id, activeTenantId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
profile = profileResult?.[0] ?? null
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// 5) PERMISSIONS — RPC ERSETZT
|
||||
// ----------------------------------------------------
|
||||
const permissionRows =
|
||||
(await server.db
|
||||
.select({
|
||||
permission: authRolePermissions.permission,
|
||||
})
|
||||
.from(authUserRoles)
|
||||
.innerJoin(
|
||||
authRoles,
|
||||
and(
|
||||
eq(authRoles.id, authUserRoles.role_id),
|
||||
or(
|
||||
isNull(authRoles.tenant_id), // globale Rolle
|
||||
eq(authRoles.tenant_id, activeTenantId) // tenant-spezifische Rolle
|
||||
)
|
||||
)
|
||||
)
|
||||
.innerJoin(
|
||||
authRolePermissions,
|
||||
eq(authRolePermissions.role_id, authRoles.id)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(authUserRoles.user_id, userId),
|
||||
eq(authUserRoles.tenant_id, activeTenantId)
|
||||
)
|
||||
)) ?? []
|
||||
|
||||
const permissions = Array.from(
|
||||
new Set(permissionRows.map((p) => p.permission))
|
||||
)
|
||||
|
||||
// ----------------------------------------------------
|
||||
// RESPONSE
|
||||
// ----------------------------------------------------
|
||||
return {
|
||||
user,
|
||||
tenants: tenantList,
|
||||
activeTenant,
|
||||
profile,
|
||||
permissions,
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("ERROR in /me route:", err)
|
||||
return reply.code(500).send({ error: "Internal server error" })
|
||||
}
|
||||
})
|
||||
}
|
||||
129
backend/src/routes/auth/user.ts
Normal file
129
backend/src/routes/auth/user.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { eq, and } from "drizzle-orm"
|
||||
|
||||
import {
|
||||
authUsers,
|
||||
authProfiles,
|
||||
} from "../../../db/schema"
|
||||
|
||||
export default async function userRoutes(server: FastifyInstance) {
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// GET /user/:id
|
||||
// -------------------------------------------------------------
|
||||
server.get("/user/:id", async (req, reply) => {
|
||||
try {
|
||||
const authUser = req.user
|
||||
const { id } = req.params as { id: string }
|
||||
|
||||
if (!authUser) {
|
||||
return reply.code(401).send({ error: "Unauthorized" })
|
||||
}
|
||||
|
||||
// 1️⃣ User laden
|
||||
const [user] = await server.db
|
||||
.select({
|
||||
id: authUsers.id,
|
||||
email: authUsers.email,
|
||||
created_at: authUsers.created_at,
|
||||
must_change_password: authUsers.must_change_password,
|
||||
})
|
||||
.from(authUsers)
|
||||
.where(eq(authUsers.id, id))
|
||||
|
||||
if (!user) {
|
||||
return reply.code(404).send({ error: "User not found" })
|
||||
}
|
||||
|
||||
// 2️⃣ Profil im Tenant
|
||||
let profile = null
|
||||
|
||||
if (authUser.tenant_id) {
|
||||
const [profileRow] = await server.db
|
||||
.select()
|
||||
.from(authProfiles)
|
||||
.where(
|
||||
and(
|
||||
eq(authProfiles.user_id, id),
|
||||
eq(authProfiles.tenant_id, authUser.tenant_id)
|
||||
)
|
||||
)
|
||||
|
||||
profile = profileRow || null
|
||||
}
|
||||
|
||||
return { user, profile }
|
||||
|
||||
} catch (err: any) {
|
||||
console.error("/user/:id ERROR", err)
|
||||
return reply.code(500).send({ error: err.message || "Internal error" })
|
||||
}
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PUT /user/:id/profile
|
||||
// -------------------------------------------------------------
|
||||
server.put("/user/:id/profile", async (req, reply) => {
|
||||
try {
|
||||
const { id } = req.params as { id: string }
|
||||
const { data } = req.body as { data?: Record<string, any> }
|
||||
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(401).send({ error: "Unauthorized" })
|
||||
}
|
||||
|
||||
if (!data || typeof data !== "object") {
|
||||
return reply.code(400).send({ error: "data object required" })
|
||||
}
|
||||
|
||||
// 1️⃣ Profil für diesen Tenant laden (damit wir die ID kennen)
|
||||
const [profile] = await server.db
|
||||
.select()
|
||||
.from(authProfiles)
|
||||
.where(
|
||||
and(
|
||||
eq(authProfiles.user_id, id),
|
||||
eq(authProfiles.tenant_id, req.user.tenant_id)
|
||||
)
|
||||
)
|
||||
|
||||
if (!profile) {
|
||||
return reply.code(404).send({ error: "Profile not found in tenant" })
|
||||
}
|
||||
|
||||
// 2️⃣ Timestamp-Felder normalisieren (falls welche drin sind)
|
||||
const normalizeDate = (val: any) => {
|
||||
if (!val) return null
|
||||
const d = new Date(val)
|
||||
return isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
const updateData: any = { ...data }
|
||||
|
||||
// bekannte Date-Felder prüfen
|
||||
if (data.entry_date !== undefined)
|
||||
updateData.entry_date = normalizeDate(data.entry_date)
|
||||
|
||||
if (data.birthday !== undefined)
|
||||
updateData.birthday = normalizeDate(data.birthday)
|
||||
|
||||
if (data.created_at !== undefined)
|
||||
updateData.created_at = normalizeDate(data.created_at)
|
||||
|
||||
updateData.updated_at = new Date()
|
||||
|
||||
// 3️⃣ Update durchführen
|
||||
const [updatedProfile] = await server.db
|
||||
.update(authProfiles)
|
||||
.set(updateData)
|
||||
.where(eq(authProfiles.id, profile.id))
|
||||
.returning()
|
||||
|
||||
return { profile: updatedProfile }
|
||||
|
||||
} catch (err: any) {
|
||||
console.error("PUT /user/:id/profile ERROR", err)
|
||||
return reply.code(500).send({ error: err.message || "Internal server error" })
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user