feat(core): setup postgres, minio, and authentik next-auth
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user