feat(core): setup postgres, minio, and authentik next-auth

This commit is contained in:
AI Assistant
2026-02-27 10:29:54 +02:00
parent 3b1ba589f0
commit 0ad7e835bd
18 changed files with 1654 additions and 105 deletions
@@ -0,0 +1,112 @@
import type { StorageService } from "../types";
export class DatabaseStorageAdapter implements StorageService {
async get<T>(namespace: string, key: string): Promise<T | null> {
try {
const res = await fetch(
`/api/storage?namespace=${encodeURIComponent(namespace)}&key=${encodeURIComponent(key)}`,
);
if (!res.ok) return null;
const data = await res.json();
return data.value as T | null;
} catch (error) {
console.error("DatabaseStorageAdapter get error:", error);
return null;
}
}
async set<T>(namespace: string, key: string, value: T): Promise<void> {
try {
await fetch("/api/storage", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ namespace, key, value }),
});
} catch (error) {
console.error("DatabaseStorageAdapter set error:", error);
}
}
async delete(namespace: string, key: string): Promise<void> {
try {
await fetch(
`/api/storage?namespace=${encodeURIComponent(namespace)}&key=${encodeURIComponent(key)}`,
{
method: "DELETE",
},
);
} catch (error) {
console.error("DatabaseStorageAdapter delete error:", error);
}
}
async list(namespace: string): Promise<string[]> {
try {
const res = await fetch(
`/api/storage?namespace=${encodeURIComponent(namespace)}`,
);
if (!res.ok) return [];
const data = await res.json();
return Object.keys(data.items || {});
} catch (error) {
console.error("DatabaseStorageAdapter list error:", error);
return [];
}
}
async query<T>(
namespace: string,
predicate: (item: T) => boolean,
): Promise<T[]> {
try {
const res = await fetch(
`/api/storage?namespace=${encodeURIComponent(namespace)}`,
);
if (!res.ok) return [];
const data = await res.json();
const items = Object.values(data.items || {}) as T[];
return items.filter(predicate);
} catch (error) {
console.error("DatabaseStorageAdapter query error:", error);
return [];
}
}
async clear(namespace: string): Promise<void> {
try {
await fetch(`/api/storage?namespace=${encodeURIComponent(namespace)}`, {
method: "DELETE",
});
} catch (error) {
console.error("DatabaseStorageAdapter clear error:", error);
}
}
async export(namespace: string): Promise<Record<string, unknown>> {
try {
const res = await fetch(
`/api/storage?namespace=${encodeURIComponent(namespace)}`,
);
if (!res.ok) return {};
const data = await res.json();
return data.items || {};
} catch (error) {
console.error("DatabaseStorageAdapter export error:", error);
return {};
}
}
async import(
namespace: string,
data: Record<string, unknown>,
): Promise<void> {
try {
// Import items one by one (or we could create a bulk endpoint, but this is fine for now)
for (const [key, value] of Object.entries(data)) {
await this.set(namespace, key, value);
}
} catch (error) {
console.error("DatabaseStorageAdapter import error:", error);
}
}
}
+33
View File
@@ -0,0 +1,33 @@
import { Client } from "minio";
const globalForMinio = globalThis as unknown as {
minioClient: Client | undefined;
};
export const minioClient =
globalForMinio.minioClient ??
new Client({
endPoint: process.env.MINIO_ENDPOINT || "localhost",
port: parseInt(process.env.MINIO_PORT || "9000"),
useSSL: process.env.MINIO_USE_SSL === "true",
accessKey: process.env.MINIO_ACCESS_KEY || "",
secretKey: process.env.MINIO_SECRET_KEY || "",
});
if (process.env.NODE_ENV !== "production")
globalForMinio.minioClient = minioClient;
export const MINIO_BUCKET_NAME = process.env.MINIO_BUCKET_NAME || "tools";
// Helper to ensure bucket exists
export async function ensureBucketExists() {
try {
const exists = await minioClient.bucketExists(MINIO_BUCKET_NAME);
if (!exists) {
await minioClient.makeBucket(MINIO_BUCKET_NAME, "eu-west-1");
console.log(`Bucket '${MINIO_BUCKET_NAME}' created successfully.`);
}
} catch (error) {
console.error("Error checking/creating MinIO bucket:", error);
}
}
+16
View File
@@ -0,0 +1,16 @@
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log:
process.env.NODE_ENV === "development"
? ["query", "error", "warn"]
: ["error"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
+13 -7
View File
@@ -1,14 +1,19 @@
'use client';
"use client";
import { createContext, useContext, useMemo } from 'react';
import type { StorageService } from './types';
import { LocalStorageAdapter } from './adapters/local-storage';
import { createContext, useContext, useMemo } from "react";
import type { StorageService } from "./types";
import { LocalStorageAdapter } from "./adapters/local-storage";
import { DatabaseStorageAdapter } from "./adapters/database-adapter";
const StorageContext = createContext<StorageService | null>(null);
function createAdapter(): StorageService {
// Future: select adapter based on environment variable
// const adapterType = process.env.NEXT_PUBLIC_STORAGE_ADAPTER;
const adapterType = process.env.NEXT_PUBLIC_STORAGE_ADAPTER;
if (adapterType === "database") {
return new DatabaseStorageAdapter();
}
return new LocalStorageAdapter();
}
@@ -28,6 +33,7 @@ export function StorageProvider({ children }: StorageProviderProps) {
export function useStorageService(): StorageService {
const ctx = useContext(StorageContext);
if (!ctx) throw new Error('useStorageService must be used within StorageProvider');
if (!ctx)
throw new Error("useStorageService must be used within StorageProvider");
return ctx;
}