Introduced New DB

This commit is contained in:
2025-12-06 10:34:58 +01:00
parent 407592680a
commit 63af22b671
80 changed files with 4509 additions and 154 deletions

View File

@@ -12,6 +12,7 @@ import authPlugin from "./plugins/auth";
import adminRoutes from "./routes/admin";
import corsPlugin from "./plugins/cors";
import queryConfigPlugin from "./plugins/queryconfig";
import dbPlugin from "./plugins/db";
import resourceRoutes from "./routes/resources";
import resourceRoutesSpecial from "./routes/resourcesSpecial";
import fastifyCookie from "@fastify/cookie";
@@ -57,6 +58,7 @@ async function main() {
await app.register(supabasePlugin);
await app.register(tenantPlugin);
await app.register(dayjsPlugin);
await app.register(dbPlugin);
app.addHook('preHandler', (req, reply, done) => {
console.log(req.method)
@@ -114,6 +116,14 @@ async function main() {
},{prefix: "/api"})
app.ready(async () => {
try {
const result = await app.db.execute("SELECT NOW()");
console.log("✓ DB connection OK: " + JSON.stringify(result.rows[0]));
} catch (err) {
console.log("❌ DB connection failed:", err);
}
});
// Start
try {

34
src/plugins/db.ts Normal file
View File

@@ -0,0 +1,34 @@
import fp from "fastify-plugin"
import {drizzle, NodePgDatabase} from "drizzle-orm/node-postgres"
import { Pool } from "pg"
import * as schema from "../../db/schema"
export default fp(async (server, opts) => {
const pool = new Pool({
host: "db-001.netbird.cloud",
port: Number(process.env.DB_PORT || 5432),
user: "postgres",
password: "wJw7aNpEBJdcxgoct6GXNpvY4Cn6ECqu",
database: "fedeo",
ssl: process.env.DB_DISABLE_SSL === "true" ? false : undefined,
})
// Drizzle instance
const db = drizzle(pool, { schema })
// Dekorieren -> überall server.db
server.decorate("db", db)
// Graceful Shutdown
server.addHook("onClose", async () => {
await pool.end()
})
server.log.info("Drizzle database connected")
})
declare module "fastify" {
interface FastifyInstance {
db:NodePgDatabase<typeof schema>
}
}

View File

@@ -1,13 +1,21 @@
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 { 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) {
// Registrierung
server.post("/auth/register",{
// -----------------------------------------------------
// REGISTER
// -----------------------------------------------------
server.post("/auth/register", {
schema: {
tags: ["Auth"],
summary: "Register User",
@@ -19,43 +27,31 @@ export default async function authRoutes(server: FastifyInstance) {
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) {
// @ts-ignore
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();
const [user] = await server.db
.insert(authUsers)
.values({
email: body.email.toLowerCase(),
passwordHash,
})
.returning({
id: authUsers.id,
email: authUsers.email,
});
if (error) {
// @ts-ignore
return reply.code(400).send({ error: error.message });
}
return { user: data };
return { user };
});
// Login
server.post("/auth/login",{
// -----------------------------------------------------
// LOGIN
// -----------------------------------------------------
server.post("/auth/login", {
schema: {
tags: ["Auth"],
summary: "Login User",
@@ -67,103 +63,110 @@ export default async function authRoutes(server: FastifyInstance) {
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) {
// @ts-ignore
return reply.code(400).send({ error: "Email and password required" });
}
let user: any = null;
/**
* 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.toLowerCase())
// -------------------------------
// SINGLE TENANT MODE
// -------------------------------
/* if (req.tenant) {
const tenantId = req.tenant.id;
// @ts-ignore
user = (data || []).find(i => i.tenants.find(x => x.id === req.tenant.id))
if(error) {
// @ts-ignore
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) {
// @ts-ignore
return reply.code(500).send({ error: "Internal Server Error" });
}
}
if(!user) {
// @ts-ignore
return reply.code(401).send({ error: "Invalid credentials" });
} else {
const valid = await bcrypt.compare(body.password, user.password_hash);
if (!valid) {
// @ts-ignore
return reply.code(401).send({ error: "Invalid credentials" });
} else {
const token = jwt.sign(
{ user_id: user.id, email: user.email, 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", // lokal: false, prod: true
maxAge: 60 * 60 * 3, // 3 Stunden
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)
));
return { token };
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 (löscht Cookie)"
},
summary: "Logout User"
}
}, async (req, reply) => {
reply.clearCookie("token", {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
})
});
return { success: true }
})
return { success: true };
});
// -----------------------------------------------------
// PASSWORD RESET
// -----------------------------------------------------
server.post("/auth/password/reset", {
schema: {
tags: ["Auth"],
@@ -177,43 +180,43 @@ export default async function authRoutes(server: FastifyInstance) {
}
}
}, async (req, reply) => {
const { email } = req.body as { email: string }
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()
const [user] = await server.db
.select({
id: authUsers.id,
email: authUsers.email,
})
.from(authUsers)
.where(eq(authUsers.email, email.toLowerCase()))
.limit(1);
if (error || !user) {
return reply.code(404).send({ error: "User not found" })
if (!user) {
return reply.code(404).send({ error: "User not found" });
}
// Neues Passwort generieren
const plainPassword = generateRandomPassword()
const passwordHash = await hashPassword(plainPassword)
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)
await server.db
.update(authUsers)
.set({
passwordHash,
mustChangePassword: true,
})
.where(eq(authUsers.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>`
)
`
<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 }
})
}
return { success: true };
});
}

View File

@@ -13,6 +13,7 @@ export let secrets = {
JWT_SECRET: string
PORT: number
HOST: string
DATABASE_URL: string
SUPABASE_URL: string
SUPABASE_SERVICE_ROLE_KEY: string
S3_BUCKET: string