feat(auth): force Authentik login on first visit, fix ManicTime sync
Auth: - Add middleware.ts that redirects unauthenticated users to Authentik SSO - Extract authOptions to shared auth-options.ts - Add getAuthSession() helper for API route protection - Add loading spinner during session validation - Dev mode bypasses auth (stub user still works) ManicTime: - Fix hardcoded companyId="beletage" — now uses group context from Tags.txt - Fix extended project format label parsing (extracts name after year) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import type { NextAuthOptions } from "next-auth";
|
||||
import AuthentikProvider from "next-auth/providers/authentik";
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
AuthentikProvider({
|
||||
clientId: process.env.AUTHENTIK_CLIENT_ID || "",
|
||||
clientSecret: process.env.AUTHENTIK_CLIENT_SECRET || "",
|
||||
issuer: process.env.AUTHENTIK_ISSUER || "",
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user, profile }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
}
|
||||
if (profile) {
|
||||
// Map Authentik groups/roles to our internal roles
|
||||
const groups = (profile as any).groups || [];
|
||||
let role = "user";
|
||||
if (groups.includes("architools-admin")) role = "admin";
|
||||
else if (groups.includes("architools-manager")) role = "manager";
|
||||
|
||||
token.role = role;
|
||||
|
||||
// Map company based on groups
|
||||
let company = "group";
|
||||
if (groups.includes("company-beletage")) company = "beletage";
|
||||
else if (groups.includes("company-urban-switch"))
|
||||
company = "urban-switch";
|
||||
else if (groups.includes("company-studii-de-teren"))
|
||||
company = "studii-de-teren";
|
||||
|
||||
token.company = company;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
(session.user as any).id = token.id;
|
||||
(session.user as any).role = token.role || "user";
|
||||
(session.user as any).company = token.company || "group";
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -30,8 +30,16 @@ interface AuthProviderProps {
|
||||
function AuthProviderInner({ children }: AuthProviderProps) {
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
// Show loading spinner while session is being validated
|
||||
if (status === "loading") {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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",
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export type { User, Role, CompanyId, AuthContextValue } from './types';
|
||||
export { AuthProvider, useAuth } from './auth-provider';
|
||||
export { authOptions } from './auth-options';
|
||||
export { getAuthSession } from './require-auth';
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "./auth-options";
|
||||
|
||||
/**
|
||||
* Get the authenticated session, or null if not authenticated.
|
||||
* Usage in API routes:
|
||||
* const session = await getAuthSession();
|
||||
* if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
*/
|
||||
export async function getAuthSession() {
|
||||
return getServerSession(authOptions);
|
||||
}
|
||||
Reference in New Issue
Block a user