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:
AI Assistant
2026-03-09 12:26:08 +02:00
parent 7ed653eaec
commit ca4d7b5d8d
7 changed files with 152 additions and 58 deletions
@@ -71,16 +71,24 @@ const COMPANY_HEADERS = new Set([
"studii de teren",
]);
/** Map group header text → CompanyId */
const HEADER_TO_COMPANY: Record<string, CompanyId> = {
beletage: "beletage",
"urban switch": "urban-switch",
"studii de teren": "studii-de-teren",
};
// ── Project line pattern: optional letter prefix + digits + space + name ──
const PROJECT_LINE_RE = /^(\w?\d+)\s+(.+)$/;
// Special pattern for "176 - 2025 - ReAC Ansamblu rezi Bibescu" style
const PROJECT_LINE_EXTENDED_RE = /^(\d+)\s+-\s+.+$/;
const PROJECT_LINE_EXTENDED_RE = /^(\d+)\s+-\s+(?:\d{4}\s+-\s+)?(.+)$/;
export interface ManicTimeTag {
line: string;
category: TagCategory | "header" | "unknown";
projectCode?: string;
companyId?: CompanyId;
label: string;
}
@@ -120,9 +128,25 @@ export function parseManicTimeFile(content: string): ParsedManicTimeFile {
}
const tags: ManicTimeTag[] = [];
let currentCompany: CompanyId | undefined;
for (const group of groups) {
// Check if the first line of this group is a company header
const firstLine = group[0];
if (firstLine) {
const headerCompany = HEADER_TO_COMPANY[firstLine.toLowerCase()];
if (headerCompany) {
currentCompany = headerCompany;
}
}
for (const line of group) {
tags.push(classifyLine(line));
const tag = classifyLine(line);
// Assign company context to project tags
if (tag.category === "project" && currentCompany) {
tag.companyId = currentCompany;
}
tags.push(tag);
}
}
@@ -155,12 +179,15 @@ function classifyLine(line: string): ManicTimeTag {
label,
};
}
if (PROJECT_LINE_EXTENDED_RE.test(trimmed)) {
const extendedMatch = trimmed.match(PROJECT_LINE_EXTENDED_RE);
if (extendedMatch?.[1] && extendedMatch[2]) {
const extNum = extendedMatch[1];
const extLabel = extendedMatch[2].trim();
return {
line: trimmed,
category: "project",
projectCode: `B-${trimmed.split(/\s/)[0]?.padStart(3, "0") ?? "000"}`,
label: trimmed,
projectCode: `B-${extNum.padStart(3, "0")}`,
label: extLabel,
};
}
@@ -345,7 +372,7 @@ export function manicTimeTagToCreateData(
};
if (category === "project") {
base.companyId = "beletage";
base.companyId = mt.companyId ?? "beletage";
base.projectCode = mt.projectCode;
}