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
+42 -20
View File
@@ -1,7 +1,8 @@
'use client';
"use client";
import { createContext, useContext, useMemo, useCallback } from 'react';
import type { AuthContextValue, User, Role } from './types';
import { createContext, useContext, useMemo, useCallback } from "react";
import { SessionProvider, useSession } from "next-auth/react";
import type { AuthContextValue, User, Role, CompanyId } from "./types";
const ROLE_HIERARCHY: Record<Role, number> = {
admin: 4,
@@ -13,55 +14,76 @@ const ROLE_HIERARCHY: Record<Role, number> = {
const AuthContext = createContext<AuthContextValue | null>(null);
// Stub user for development (no auth required)
// Stub user for development fallback
const STUB_USER: User = {
id: 'dev-user',
name: 'Utilizator Intern',
email: 'dev@architools.local',
role: 'admin',
company: 'beletage',
id: "dev-user",
name: "Utilizator Intern",
email: "dev@architools.local",
role: "admin",
company: "beletage",
};
interface AuthProviderProps {
children: React.ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
// In the current phase, always return the stub user
// Future: replace with Authentik OIDC token resolution
const user = STUB_USER;
function AuthProviderInner({ children }: AuthProviderProps) {
const { data: session, status } = useSession();
// Use session user if available, otherwise fallback to stub in dev mode
// In production, we should probably force login if no session
const user: User | null = session?.user
? {
id: (session.user as any).id || "unknown",
name: session.user.name || "Unknown User",
email: session.user.email || "",
role: ((session.user as any).role as Role) || "user",
company: ((session.user as any).company as CompanyId) || "group",
}
: process.env.NODE_ENV === "development"
? STUB_USER
: null;
const hasRole = useCallback(
(requiredRole: Role) => {
if (!user) return false;
return ROLE_HIERARCHY[user.role] >= ROLE_HIERARCHY[requiredRole];
},
[user.role]
[user],
);
const canAccessModule = useCallback(
(_moduleId: string) => {
// Future: check module-level permissions
return true;
return !!user;
},
[]
[user],
);
const value: AuthContextValue = useMemo(
() => ({
user,
role: user.role,
isAuthenticated: true,
role: user?.role || "guest",
isAuthenticated: !!user,
hasRole,
canAccessModule,
}),
[user, hasRole, canAccessModule]
[user, hasRole, canAccessModule],
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function AuthProvider({ children }: AuthProviderProps) {
return (
<SessionProvider>
<AuthProviderInner>{children}</AuthProviderInner>
</SessionProvider>
);
}
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}
@@ -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;
}