Added Backend

This commit is contained in:
2026-01-06 12:07:43 +01:00
parent b013ef8f4b
commit 6f3d4c0bff
165 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
import { FastifyInstance } from "fastify";
import fp from "fastify-plugin";
import { secrets } from "../utils/secrets";
/**
* Fastify Plugin für Machine-to-Machine Authentifizierung.
*
* Dieses Plugin prüft, ob der Header `x-api-key` vorhanden ist
* und mit dem in der .env hinterlegten M2M_API_KEY übereinstimmt.
*
* Verwendung:
* server.register(m2mAuthPlugin, { allowedPrefix: '/internal' })
*/
export default fp(async (server: FastifyInstance, opts: { allowedPrefix?: string } = {}) => {
//const allowedPrefix = opts.allowedPrefix || "/internal";
server.addHook("preHandler", async (req, reply) => {
try {
// Nur prüfen, wenn Route unterhalb des Prefix liegt
//if (!req.url.startsWith(allowedPrefix)) return;
const apiKey = req.headers["x-api-key"];
if (!apiKey || apiKey !== secrets.M2M_API_KEY) {
server.log.warn(`[M2M Auth] Ungültiger oder fehlender API-Key bei ${req.url}`);
return reply.status(401).send({ error: "Unauthorized" });
}
// Zusatzinformationen im Request (z. B. interne Kennung)
(req as any).m2m = {
verified: true,
type: "internal",
key: apiKey,
};
} catch (err) {
// @ts-ignore
server.log.error("[M2M Auth] Fehler beim Prüfen des API-Keys:", err);
return reply.status(500).send({ error: "Internal Server Error" });
}
});
});
declare module "fastify" {
interface FastifyRequest {
m2m?: {
verified: boolean;
type: "internal";
key: string;
};
}
}

115
backend/src/plugins/auth.ts Normal file
View File

@@ -0,0 +1,115 @@
import { FastifyInstance } from "fastify"
import fp from "fastify-plugin"
import jwt from "jsonwebtoken"
import { secrets } from "../utils/secrets"
import {
authUserRoles,
authRolePermissions,
} from "../../db/schema"
import { eq, and } from "drizzle-orm"
export default fp(async (server: FastifyInstance) => {
server.addHook("preHandler", async (req, reply) => {
// 1⃣ Token aus Header oder Cookie lesen
const cookieToken = req.cookies?.token
const authHeader = req.headers.authorization
const headerToken =
authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null
const token =
headerToken && headerToken.length > 10
? headerToken
: cookieToken || null
if (!token) {
return reply.code(401).send({ error: "Authentication required" })
}
try {
// 2⃣ JWT verifizieren
const payload = jwt.verify(token, secrets.JWT_SECRET!) as {
user_id: string
email: string
tenant_id: number | null
}
if (!payload?.user_id) {
return reply.code(401).send({ error: "Invalid token" })
}
// Payload an Request hängen
req.user = payload
// Multi-Tenant Modus ohne ausgewählten Tenant → keine Rollenprüfung
if (!req.user.tenant_id) {
return
}
const tenantId = req.user.tenant_id
const userId = req.user.user_id
// --------------------------------------------------------
// 3⃣ Rolle des Nutzers im Tenant holen
// --------------------------------------------------------
const roleRows = await server.db
.select()
.from(authUserRoles)
.where(
and(
eq(authUserRoles.user_id, userId),
eq(authUserRoles.tenant_id, tenantId)
)
)
.limit(1)
if (roleRows.length === 0) {
return reply
.code(403)
.send({ error: "No role assigned for this tenant" })
}
const roleId = roleRows[0].role_id
// --------------------------------------------------------
// 4⃣ Berechtigungen der Rolle laden
// --------------------------------------------------------
const permissionRows = await server.db
.select()
.from(authRolePermissions)
.where(eq(authRolePermissions.role_id, roleId))
const permissions = permissionRows.map((p) => p.permission)
// --------------------------------------------------------
// 5⃣ An Request hängen für spätere Nutzung
// --------------------------------------------------------
req.role = roleId
req.permissions = permissions
req.hasPermission = (perm: string) => permissions.includes(perm)
} catch (err) {
console.error("JWT verification error:", err)
return reply.code(401).send({ error: "Invalid or expired token" })
}
})
})
// ---------------------------------------------------------------------------
// Fastify TypeScript Erweiterungen
// ---------------------------------------------------------------------------
declare module "fastify" {
interface FastifyRequest {
user: {
user_id: string
email: string
tenant_id: number | null
}
role: string
permissions: string[]
hasPermission: (permission: string) => boolean
}
}

View File

@@ -0,0 +1,22 @@
import { FastifyInstance } from "fastify";
import fp from "fastify-plugin";
import cors from "@fastify/cors";
export default fp(async (server: FastifyInstance) => {
await server.register(cors, {
origin: [
"http://localhost:3000", // dein Nuxt-Frontend
"http://localhost:3001", // dein Nuxt-Frontend
"http://127.0.0.1:3000", // dein Nuxt-Frontend
"http://192.168.1.227:3001", // dein Nuxt-Frontend
"http://192.168.1.227:3000", // dein Nuxt-Frontend
"https://beta.fedeo.de", // dein Nuxt-Frontend
"https://app.fedeo.de", // dein Nuxt-Frontend
"capacitor://localhost", // dein Nuxt-Frontend
],
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "Context", "X-Public-Pin"],
exposedHeaders: ["Authorization", "Content-Disposition", "Content-Type", "Content-Length"], // optional, falls du ihn auch auslesen willst
credentials: true, // wichtig, falls du Cookies nutzt
});
});

View File

@@ -0,0 +1,41 @@
import fp from "fastify-plugin"
import dayjs from "dayjs"
// 🧩 Plugins
import customParseFormat from "dayjs/plugin/customParseFormat.js";
import isBetween from "dayjs/plugin/isBetween.js";
import duration from "dayjs/plugin/duration.js";
import utc from "dayjs/plugin/utc"
import timezone from "dayjs/plugin/timezone"
import isSameOrAfter from "dayjs/plugin/isSameOrAfter"
import isSameOrBefore from "dayjs/plugin/isSameOrBefore"
import isoWeek from "dayjs/plugin/isoWeek"
import localizedFormat from "dayjs/plugin/localizedFormat"
// 🔧 Erweiterungen aktivieren
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(isSameOrAfter)
dayjs.extend(isSameOrBefore)
dayjs.extend(isBetween)
dayjs.extend(isoWeek)
dayjs.extend(localizedFormat)
dayjs.extend(customParseFormat)
dayjs.extend(isBetween)
dayjs.extend(duration)
/**
* Fastify Plugin: hängt dayjs an den Server an
*/
export default fp(async (server) => {
server.decorate("dayjs", dayjs)
})
/**
* Typ-Erweiterung für TypeScript
*/
declare module "fastify" {
interface FastifyInstance {
dayjs: typeof dayjs
}
}

34
backend/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: "100.102.185.225",
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

@@ -0,0 +1,125 @@
import fp from 'fastify-plugin'
import { FastifyPluginAsync, FastifyRequest } from 'fastify'
export interface QueryConfigPagination {
page: number
limit: number
offset: number
}
export interface QueryConfigSort {
field: string
direction: 'asc' | 'desc'
}
export interface QueryConfig {
pagination: QueryConfigPagination | null
sort: QueryConfigSort[]
filters: Record<string, string>
paginationDisabled: boolean
}
declare module 'fastify' {
interface FastifyRequest {
queryConfig: QueryConfig
}
}
interface QueryConfigPluginOptions {
routes?: string[]
}
function matchRoutePattern(currentPath: string, patterns: string[]): boolean {
return patterns.some(pattern => {
// Beispiel: /users/:id -> /^\/users\/[^/]+$/
const regex = new RegExp(
'^' +
pattern
.replace(/\*/g, '.*') // wildcard
.replace(/:[^/]+/g, '[^/]+') +
'$'
)
return regex.test(currentPath)
})
}
const queryConfigPlugin: FastifyPluginAsync<QueryConfigPluginOptions> = async (
fastify,
opts
) => {
const routePatterns = opts.routes || []
fastify.addHook('preHandler', async (req: FastifyRequest, reply) => {
const path = req.routeOptions.url || req.raw.url || ''
if (!matchRoutePattern(path, routePatterns)) {
return
}
const query = req.query as Record<string, any>
console.log(query)
// Pagination deaktivieren?
const disablePagination =
query.noPagination === 'true' ||
query.pagination === 'false' ||
query.limit === '0'
// Pagination berechnen
let pagination: QueryConfigPagination | null = null
if (!disablePagination) {
const page = Math.max(parseInt(query.page) || 1, 1)
const limit = Math.max(parseInt(query.limit) || 25, 1)
const offset = (page - 1) * limit
pagination = { page, limit, offset }
}
// Sortierung
const sort: QueryConfigSort[] = []
if (typeof query.sort === 'string') {
const items = query.sort.split(',')
for (const item of items) {
const [field, direction] = item.split(':')
sort.push({
field: field.trim(),
direction: (direction || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc'
})
}
}
// Filterung
const filters: Record<string, any> = {}
for (const [key, value] of Object.entries(query)) {
const match = key.match(/^filter\[(.+)\]$/)
if (!match) continue
const filterKey = match[1]
if (typeof value === 'string') {
// Split bei Komma → mehrere Werte
const parts = value.split(',').map(v => v.trim()).filter(Boolean)
// Automatische Typkonvertierung je Element
const parsedValues = parts.map(v => {
if (v === 'true') return true
if (v === 'false') return false
if (v === 'null') return null
return v
})
filters[filterKey] = parsedValues.length > 1 ? parsedValues : parsedValues[0]
}
}
req.queryConfig = {
pagination,
sort,
filters,
paginationDisabled: disablePagination
}
})
}
export default fp(queryConfigPlugin, { name: 'query-config' })

View File

@@ -0,0 +1,24 @@
// /plugins/services.ts
import fp from "fastify-plugin";
import { bankStatementService } from "../modules/cron/bankstatementsync.service";
//import {initDokuboxClient, syncDokubox} from "../modules/cron/dokuboximport.service";
import { FastifyInstance } from "fastify";
import {prepareIncomingInvoices} from "../modules/cron/prepareIncomingInvoices";
declare module "fastify" {
interface FastifyInstance {
services: {
bankStatements: ReturnType<typeof bankStatementService>;
//dokuboxSync: ReturnType<typeof syncDokubox>;
prepareIncomingInvoices: ReturnType<typeof prepareIncomingInvoices>;
};
}
}
export default fp(async function servicePlugin(server: FastifyInstance) {
server.decorate("services", {
bankStatements: bankStatementService(server),
//dokuboxSync: syncDokubox(server),
prepareIncomingInvoices: prepareIncomingInvoices(server),
});
});

View File

@@ -0,0 +1,19 @@
import { FastifyInstance } from "fastify";
import fp from "fastify-plugin";
import { createClient, SupabaseClient } from "@supabase/supabase-js";
import {secrets} from "../utils/secrets";
export default fp(async (server: FastifyInstance) => {
const supabaseUrl = secrets.SUPABASE_URL
const supabaseServiceKey = secrets.SUPABASE_SERVICE_ROLE_KEY
const supabase: SupabaseClient = createClient(supabaseUrl, supabaseServiceKey);
// Fastify um supabase erweitern
server.decorate("supabase", supabase);
});
declare module "fastify" {
interface FastifyInstance {
supabase: SupabaseClient;
}
}

View File

@@ -0,0 +1,30 @@
import { FastifyInstance } from "fastify";
import fp from "fastify-plugin";
import swagger from "@fastify/swagger";
import swaggerUi from "@fastify/swagger-ui";
export default fp(async (server: FastifyInstance) => {
await server.register(swagger, {
mode: "dynamic", // wichtig: generiert echtes OpenAPI JSON
openapi: {
info: {
title: "Multi-Tenant API",
description: "API Dokumentation für dein Backend",
version: "1.0.0",
},
servers: [{ url: "http://localhost:3000" }],
},
});
// @ts-ignore
await server.register(swaggerUi, {
routePrefix: "/docs", // UI erreichbar unter http://localhost:3000/docs
swagger: {
info: {
title: "Multi-Tenant API",
version: "1.0.0",
},
},
exposeRoute: true,
});
});

View File

@@ -0,0 +1,41 @@
import { FastifyInstance, FastifyRequest } from "fastify";
import fp from "fastify-plugin";
export default fp(async (server: FastifyInstance) => {
server.addHook("preHandler", async (req, reply) => {
const host = req.headers.host?.split(":")[0]; // Domain ohne Port
if (!host) {
reply.code(400).send({ error: "Missing host header" });
return;
}
// Tenant aus DB laden
const { data: tenant } = await server.supabase
.from("tenants")
.select("*")
.eq("portalDomain", host)
.single();
if(!tenant) {
// Multi Tenant Mode
(req as any).tenant = null;
}else {
// Tenant ins Request-Objekt hängen
(req as any).tenant = tenant;
}
});
});
// Typ-Erweiterung
declare module "fastify" {
interface FastifyRequest {
tenant?: {
id: string;
name: string;
domain?: string;
subdomain?: string;
settings?: Record<string, any>;
};
}
}