tina
This commit is contained in:
47
tina-backend/Dockerfile
Normal file
47
tina-backend/Dockerfile
Normal file
@@ -0,0 +1,47 @@
|
||||
# Étape 1: Build de l'admin Tina
|
||||
FROM node:24-alpine AS tina-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copie des fichiers nécessaires pour Tina
|
||||
COPY package*.json ./
|
||||
COPY tina ./tina
|
||||
COPY public ./public
|
||||
COPY tsconfig.json ./
|
||||
|
||||
# Installation des dépendances
|
||||
RUN npm install
|
||||
|
||||
# Build de l'admin Tina
|
||||
RUN npx tinacms build
|
||||
|
||||
# Étape 2: Build du backend Fastify
|
||||
FROM node:20-alpine AS backend-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY tina-backend/package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY tina-backend/tsconfig.json ./
|
||||
COPY tina-backend/src ./src
|
||||
COPY tina-backend/drizzle.config.ts ./
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Étape 3: Image finale
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copie du backend buildé
|
||||
COPY --from=backend-builder /app/dist ./dist
|
||||
COPY --from=backend-builder /app/node_modules ./node_modules
|
||||
COPY --from=backend-builder /app/package*.json ./
|
||||
|
||||
# Copie de l'admin Tina buildé
|
||||
COPY --from=tina-builder /app/admin ./admin
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
10
tina-backend/drizzle.config.ts
Normal file
10
tina-backend/drizzle.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { Config } from "drizzle-kit";
|
||||
|
||||
export default {
|
||||
schema: "./src/db/schema.ts",
|
||||
out: "./migrations",
|
||||
driver: "pg",
|
||||
dbCredentials: {
|
||||
connectionString: process.env.DATABASE_URL || "",
|
||||
},
|
||||
} satisfies Config;
|
||||
25
tina-backend/migrations/001_initial.sql
Normal file
25
tina-backend/migrations/001_initial.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
CREATE TABLE IF NOT EXISTS tina_kv (
|
||||
key VARCHAR(512) NOT NULL,
|
||||
value JSONB NOT NULL,
|
||||
namespace VARCHAR(100) NOT NULL DEFAULT 'main',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (key, namespace)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_namespace ON tina_kv(namespace);
|
||||
CREATE INDEX IF NOT EXISTS idx_updated ON tina_kv(updated_at);
|
||||
|
||||
-- Fonction pour mise à jour automatique du updated_at
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
CREATE TRIGGER update_tina_kv_updated_at
|
||||
BEFORE UPDATE ON tina_kv
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
33
tina-backend/package.json
Normal file
33
tina-backend/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "tina-backend",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"start": "node dist/index.js",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cookie": "^9.3.1",
|
||||
"@fastify/cors": "^9.0.1",
|
||||
"@fastify/static": "^7.0.1",
|
||||
"@tinacms/datalayer": "^1.2.0",
|
||||
"abstract-level": "^1.0.4",
|
||||
"drizzle-orm": "^0.30.0",
|
||||
"fastify": "^4.26.0",
|
||||
"iron-session": "^8.0.1",
|
||||
"openid-client": "^5.6.0",
|
||||
"pg": "^8.11.0",
|
||||
"tinacms-gitprovider-github": "^1.0.0",
|
||||
"zod": "^3.22.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/pg": "^8.11.0",
|
||||
"drizzle-kit": "^0.20.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
147
tina-backend/src/auth/oidc-backend.ts
Normal file
147
tina-backend/src/auth/oidc-backend.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { BackendAuthProvider } from "@tinacms/datalayer";
|
||||
import { FastifyRequest, FastifyReply } from "fastify";
|
||||
import { getSession } from "./session";
|
||||
import { Issuer, generators } from "openid-client";
|
||||
|
||||
// Stockage temporaire des states (en production, utiliser Redis)
|
||||
const stateStore = new Map<string, { timestamp: number }>();
|
||||
|
||||
// Nettoyage périodique des states expirés (5 minutes)
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [state, data] of stateStore.entries()) {
|
||||
if (now - data.timestamp > 5 * 60 * 1000) {
|
||||
stateStore.delete(state);
|
||||
}
|
||||
}
|
||||
}, 60 * 1000);
|
||||
|
||||
async function getOIDCClient() {
|
||||
const issuer = await Issuer.discover(process.env.OIDC_ISSUER!);
|
||||
return new issuer.Client({
|
||||
client_id: process.env.OIDC_CLIENT_ID!,
|
||||
client_secret: process.env.OIDC_CLIENT_SECRET!,
|
||||
redirect_uris: [process.env.OIDC_CALLBACK_URL!],
|
||||
response_types: ["code"],
|
||||
});
|
||||
}
|
||||
|
||||
function generateState(): string {
|
||||
const state = generators.state();
|
||||
stateStore.set(state, { timestamp: Date.now() });
|
||||
return state;
|
||||
}
|
||||
|
||||
function validateState(state: string): boolean {
|
||||
const data = stateStore.get(state);
|
||||
if (!data) return false;
|
||||
|
||||
// Vérifier que le state n'est pas expiré (5 minutes)
|
||||
if (Date.now() - data.timestamp > 5 * 60 * 1000) {
|
||||
stateStore.delete(state);
|
||||
return false;
|
||||
}
|
||||
|
||||
stateStore.delete(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
export const createOIDCBackendAuth = (): BackendAuthProvider => {
|
||||
return {
|
||||
isAuthorized: async (req: FastifyRequest, res: FastifyReply) => {
|
||||
const session = await getSession(req, res);
|
||||
|
||||
if (!session.user) {
|
||||
return {
|
||||
isAuthorized: false,
|
||||
errorMessage: "Unauthorized",
|
||||
errorCode: 401,
|
||||
};
|
||||
}
|
||||
|
||||
return { isAuthorized: true };
|
||||
},
|
||||
|
||||
extraRoutes: {
|
||||
"/auth/login": {
|
||||
secure: false,
|
||||
handler: async (req, res) => {
|
||||
const client = await getOIDCClient();
|
||||
const state = generateState();
|
||||
|
||||
const authorizationUrl = client.authorizationUrl({
|
||||
scope: process.env.OIDC_SCOPE || "openid profile email",
|
||||
state,
|
||||
});
|
||||
|
||||
res.redirect(authorizationUrl);
|
||||
},
|
||||
},
|
||||
|
||||
"/auth/callback": {
|
||||
secure: false,
|
||||
handler: async (req, res) => {
|
||||
try {
|
||||
const client = await getOIDCClient();
|
||||
const params = client.callbackParams(req.raw);
|
||||
|
||||
// Vérifier le state pour prévenir les attaques CSRF
|
||||
if (!params.state || !validateState(params.state)) {
|
||||
return res.status(400).send({ error: "Invalid state parameter" });
|
||||
}
|
||||
|
||||
// Échanger le code contre des tokens
|
||||
const tokenSet = await client.callback(
|
||||
process.env.OIDC_CALLBACK_URL!,
|
||||
params,
|
||||
{ state: params.state }
|
||||
);
|
||||
|
||||
// Valider et extraire les claims
|
||||
const claims = tokenSet.claims();
|
||||
|
||||
// Créer la session
|
||||
const session = await getSession(req, res);
|
||||
session.user = {
|
||||
id: claims.sub,
|
||||
email: claims.email || "",
|
||||
name: claims.name || claims.preferred_username || "",
|
||||
};
|
||||
session.accessToken = tokenSet.access_token;
|
||||
session.idToken = tokenSet.id_token;
|
||||
|
||||
await session.save();
|
||||
|
||||
// Rediriger vers l'admin
|
||||
res.redirect("/admin");
|
||||
} catch (error) {
|
||||
console.error("OIDC callback error:", error);
|
||||
res.status(500).send({ error: "Authentication failed" });
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
"/auth/logout": {
|
||||
secure: false,
|
||||
handler: async (req, res) => {
|
||||
const session = await getSession(req, res);
|
||||
session.destroy();
|
||||
res.redirect("/admin");
|
||||
},
|
||||
},
|
||||
|
||||
"/api/me": {
|
||||
secure: false,
|
||||
handler: async (req, res) => {
|
||||
const session = await getSession(req, res);
|
||||
|
||||
if (!session.user) {
|
||||
return res.status(401).send({ error: "Not authenticated" });
|
||||
}
|
||||
|
||||
return session.user;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
27
tina-backend/src/auth/session.ts
Normal file
27
tina-backend/src/auth/session.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { getIronSession, IronSessionOptions } from "iron-session";
|
||||
import { FastifyRequest, FastifyReply } from "fastify";
|
||||
|
||||
export interface SessionData {
|
||||
user?: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
accessToken?: string;
|
||||
idToken?: string;
|
||||
}
|
||||
|
||||
const sessionOptions: IronSessionOptions = {
|
||||
password: process.env.SESSION_SECRET!,
|
||||
cookieName: "tina_session",
|
||||
cookieOptions: {
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 jours
|
||||
},
|
||||
};
|
||||
|
||||
export async function getSession(req: FastifyRequest, res: FastifyReply) {
|
||||
return getIronSession<SessionData>(req.raw, res.raw, sessionOptions);
|
||||
}
|
||||
28
tina-backend/src/config.ts
Normal file
28
tina-backend/src/config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const configSchema = z.object({
|
||||
// Database
|
||||
DATABASE_URL: z.string().url(),
|
||||
|
||||
// OIDC Configuration
|
||||
OIDC_ISSUER: z.string().url(),
|
||||
OIDC_CLIENT_ID: z.string(),
|
||||
OIDC_CLIENT_SECRET: z.string(),
|
||||
OIDC_CALLBACK_URL: z.string().url(),
|
||||
OIDC_SCOPE: z.string().default("openid profile email"),
|
||||
|
||||
// Session (Iron)
|
||||
SESSION_SECRET: z.string().min(32),
|
||||
|
||||
// GitHub
|
||||
GITHUB_TOKEN: z.string(),
|
||||
GITHUB_REPO: z.string(),
|
||||
GITHUB_OWNER: z.string(),
|
||||
GITHUB_BRANCH: z.string().default("main"),
|
||||
|
||||
// Server
|
||||
PORT: z.string().default("3000"),
|
||||
NODE_ENV: z.enum(["development", "production"]).default("development"),
|
||||
});
|
||||
|
||||
export const config = configSchema.parse(process.env);
|
||||
234
tina-backend/src/database/postgres-adapter.ts
Normal file
234
tina-backend/src/database/postgres-adapter.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { AbstractLevel, AbstractIterator, AbstractDatabaseOptions } from "abstract-level";
|
||||
import { db } from "../db";
|
||||
import { tinaKv } from "../db/schema";
|
||||
import { eq, and, gte, lt, sql } from "drizzle-orm";
|
||||
|
||||
// Types
|
||||
interface NodeCallback<T> {
|
||||
(err: Error | null, result?: T): void;
|
||||
}
|
||||
|
||||
interface IteratorOptions {
|
||||
gt?: string;
|
||||
gte?: string;
|
||||
lt?: string;
|
||||
lte?: string;
|
||||
reverse?: boolean;
|
||||
limit?: number;
|
||||
keys?: boolean;
|
||||
values?: boolean;
|
||||
}
|
||||
|
||||
interface PostgresLevelOptions {
|
||||
connectionString: string;
|
||||
namespace?: string;
|
||||
}
|
||||
|
||||
// Iterator personnalisé pour PostgreSQL
|
||||
class PostgresIterator extends AbstractIterator<string, any> {
|
||||
private namespace: string;
|
||||
private options: IteratorOptions;
|
||||
private results: { key: string; value: any }[] = [];
|
||||
private currentIndex = 0;
|
||||
private initialized = false;
|
||||
|
||||
constructor(db: any, options: IteratorOptions, namespace: string) {
|
||||
super(db);
|
||||
this.options = options;
|
||||
this.namespace = namespace;
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
// Construire la requête
|
||||
let query = db
|
||||
.select({
|
||||
key: tinaKv.key,
|
||||
value: tinaKv.value,
|
||||
})
|
||||
.from(tinaKv)
|
||||
.where(eq(tinaKv.namespace, this.namespace));
|
||||
|
||||
// Gestion des bounds
|
||||
const conditions = [];
|
||||
|
||||
if (this.options.gt !== undefined) {
|
||||
conditions.push(sql`${tinaKv.key} > ${this.options.gt}`);
|
||||
}
|
||||
if (this.options.gte !== undefined) {
|
||||
conditions.push(sql`${tinaKv.key} >= ${this.options.gte}`);
|
||||
}
|
||||
if (this.options.lt !== undefined) {
|
||||
conditions.push(sql`${tinaKv.key} < ${this.options.lt}`);
|
||||
}
|
||||
if (this.options.lte !== undefined) {
|
||||
conditions.push(sql`${tinaKv.key} <= ${this.options.lte}`);
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
query = query.where(and(...conditions, eq(tinaKv.namespace, this.namespace)));
|
||||
}
|
||||
|
||||
// Ordre
|
||||
if (this.options.reverse) {
|
||||
query = query.orderBy(sql`${tinaKv.key} DESC`);
|
||||
} else {
|
||||
query = query.orderBy(tinaKv.key);
|
||||
}
|
||||
|
||||
// Limite
|
||||
if (this.options.limit !== undefined && this.options.limit > 0) {
|
||||
query = query.limit(this.options.limit);
|
||||
}
|
||||
|
||||
this.results = await query;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
async _next(callback: NodeCallback<{ key: string; value: any }>): Promise<void> {
|
||||
try {
|
||||
await this.initialize();
|
||||
|
||||
if (this.currentIndex >= this.results.length) {
|
||||
return callback(null, undefined as any);
|
||||
}
|
||||
|
||||
const item = this.results[this.currentIndex++];
|
||||
|
||||
// Gestion des options keys/values
|
||||
const result: { key?: string; value?: any } = {};
|
||||
|
||||
if (this.options.keys !== false) {
|
||||
result.key = item.key;
|
||||
}
|
||||
if (this.options.values !== false) {
|
||||
result.value = item.value;
|
||||
}
|
||||
|
||||
callback(null, result as { key: string; value: any });
|
||||
} catch (error) {
|
||||
callback(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
_seek(target: string): void {
|
||||
this.currentIndex = this.results.findIndex(r => r.key >= target);
|
||||
if (this.currentIndex < 0) {
|
||||
this.currentIndex = this.results.length;
|
||||
}
|
||||
}
|
||||
|
||||
_end(callback: NodeCallback<void>): void {
|
||||
this.results = [];
|
||||
this.initialized = false;
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Classe principale PostgreSQL Level
|
||||
export class PostgresLevel extends AbstractLevel<string, any> {
|
||||
private namespace: string;
|
||||
|
||||
constructor(options: PostgresLevelOptions) {
|
||||
// @ts-ignore - abstract-level constructor accepte des options
|
||||
super({
|
||||
keyEncoding: "utf8",
|
||||
valueEncoding: "json",
|
||||
});
|
||||
this.namespace = options.namespace || "main";
|
||||
}
|
||||
|
||||
async _put(key: string, value: any): Promise<void> {
|
||||
await db
|
||||
.insert(tinaKv)
|
||||
.values({
|
||||
key,
|
||||
value,
|
||||
namespace: this.namespace,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [tinaKv.key, tinaKv.namespace],
|
||||
set: { value },
|
||||
});
|
||||
}
|
||||
|
||||
async _get(key: string): Promise<any> {
|
||||
const result = await db
|
||||
.select({ value: tinaKv.value })
|
||||
.from(tinaKv)
|
||||
.where(and(eq(tinaKv.key, key), eq(tinaKv.namespace, this.namespace)))
|
||||
.limit(1);
|
||||
|
||||
if (result.length === 0) {
|
||||
const error = new Error(`Key not found: ${key}`) as Error & { notFound?: boolean; status?: number };
|
||||
error.notFound = true;
|
||||
error.status = 404;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return result[0].value;
|
||||
}
|
||||
|
||||
async _del(key: string): Promise<void> {
|
||||
await db
|
||||
.delete(tinaKv)
|
||||
.where(and(eq(tinaKv.key, key), eq(tinaKv.namespace, this.namespace)));
|
||||
}
|
||||
|
||||
async _batch(operations: Array<{ type: "put" | "del"; key: string; value?: any }>): Promise<void> {
|
||||
// Utiliser une transaction pour les opérations batch
|
||||
await db.transaction(async (tx) => {
|
||||
for (const op of operations) {
|
||||
if (op.type === "put") {
|
||||
await tx
|
||||
.insert(tinaKv)
|
||||
.values({
|
||||
key: op.key,
|
||||
value: op.value,
|
||||
namespace: this.namespace,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [tinaKv.key, tinaKv.namespace],
|
||||
set: { value: op.value },
|
||||
});
|
||||
} else if (op.type === "del") {
|
||||
await tx
|
||||
.delete(tinaKv)
|
||||
.where(and(eq(tinaKv.key, op.key), eq(tinaKv.namespace, this.namespace)));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_iterator(options: IteratorOptions): PostgresIterator {
|
||||
return new PostgresIterator(this, options, this.namespace);
|
||||
}
|
||||
|
||||
async _clear(options: { gt?: string; gte?: string; lt?: string; lte?: string }): Promise<void> {
|
||||
let query = db.delete(tinaKv).where(eq(tinaKv.namespace, this.namespace));
|
||||
|
||||
const conditions = [];
|
||||
|
||||
if (options.gt !== undefined) {
|
||||
conditions.push(sql`${tinaKv.key} > ${options.gt}`);
|
||||
}
|
||||
if (options.gte !== undefined) {
|
||||
conditions.push(sql`${tinaKv.key} >= ${options.gte}`);
|
||||
}
|
||||
if (options.lt !== undefined) {
|
||||
conditions.push(sql`${tinaKv.key} < ${options.lt}`);
|
||||
}
|
||||
if (options.lte !== undefined) {
|
||||
conditions.push(sql`${tinaKv.key} <= ${options.lte}`);
|
||||
}
|
||||
|
||||
if (conditions.length > 0) {
|
||||
query = db
|
||||
.delete(tinaKv)
|
||||
.where(and(...conditions, eq(tinaKv.namespace, this.namespace)));
|
||||
}
|
||||
|
||||
await query;
|
||||
}
|
||||
}
|
||||
9
tina-backend/src/db/index.ts
Normal file
9
tina-backend/src/db/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
import { Pool } from "pg";
|
||||
import * as schema from "./schema";
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
|
||||
export const db = drizzle(pool, { schema });
|
||||
11
tina-backend/src/db/schema.ts
Normal file
11
tina-backend/src/db/schema.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { pgTable, varchar, jsonb, timestamp, primaryKey } from "drizzle-orm/pg-core";
|
||||
|
||||
export const tinaKv = pgTable("tina_kv", {
|
||||
key: varchar("key", { length: 512 }).notNull(),
|
||||
value: jsonb("value").notNull(),
|
||||
namespace: varchar("namespace", { length: 100 }).notNull().default("main"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
||||
}, (table) => ({
|
||||
pk: primaryKey({ columns: [table.key, table.namespace] }),
|
||||
}));
|
||||
79
tina-backend/src/index.ts
Normal file
79
tina-backend/src/index.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import Fastify from "fastify";
|
||||
import cors from "@fastify/cors";
|
||||
import cookie from "@fastify/cookie";
|
||||
import staticFiles from "@fastify/static";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { TinaNodeBackend, createDatabase } from "@tinacms/datalayer";
|
||||
import { GitHubProvider } from "tinacms-gitprovider-github";
|
||||
import { createOIDCBackendAuth } from "./auth/oidc-backend.js";
|
||||
import { PostgresLevel } from "./database/postgres-adapter.js";
|
||||
import { config } from "./config.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const fastify = Fastify({
|
||||
logger: true,
|
||||
});
|
||||
|
||||
// Plugins
|
||||
await fastify.register(cors, {
|
||||
origin: true,
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
await fastify.register(cookie);
|
||||
|
||||
// Static files (Tina Admin build)
|
||||
await fastify.register(staticFiles, {
|
||||
root: path.join(__dirname, "../admin"),
|
||||
prefix: "/admin/",
|
||||
});
|
||||
|
||||
// Database Adapter
|
||||
const dbAdapter = new PostgresLevel({
|
||||
connectionString: config.DATABASE_URL,
|
||||
namespace: config.GITHUB_BRANCH,
|
||||
});
|
||||
|
||||
// Git Provider
|
||||
const gitProvider = new GitHubProvider({
|
||||
repo: config.GITHUB_REPO,
|
||||
owner: config.GITHUB_OWNER,
|
||||
token: config.GITHUB_TOKEN,
|
||||
branch: config.GITHUB_BRANCH,
|
||||
});
|
||||
|
||||
// Create Database
|
||||
const database = createDatabase({
|
||||
gitProvider,
|
||||
databaseAdapter: dbAdapter,
|
||||
});
|
||||
|
||||
// Tina Backend Handler
|
||||
const tinaBackend = TinaNodeBackend({
|
||||
authProvider: createOIDCBackendAuth(),
|
||||
databaseClient: database,
|
||||
});
|
||||
|
||||
// Routes
|
||||
fastify.all("/api/tina/*", async (request, reply) => {
|
||||
await tinaBackend(request.raw, reply.raw);
|
||||
});
|
||||
|
||||
// Route racine pour /admin
|
||||
fastify.get("/admin", async (request, reply) => {
|
||||
return reply.sendFile("index.html");
|
||||
});
|
||||
|
||||
// Health check
|
||||
fastify.get("/health", async () => {
|
||||
return { status: "ok" };
|
||||
});
|
||||
|
||||
// Start server
|
||||
fastify.listen({
|
||||
port: parseInt(config.PORT),
|
||||
host: "0.0.0.0",
|
||||
});
|
||||
16
tina-backend/tsconfig.json
Normal file
16
tina-backend/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user