feat: Implement Keycloak authentication and a basic CMS

Integrates Keycloak for secure administrator authentication using Arctic.
Introduces a full CMS dashboard for managing CV content, supporting multi-language editing for profile, experience, and education sections.

Refactors application routes for modularity and adds initial unit tests for database queries.

Also includes minor UI/UX refinements, animation setup, and local Keycloak docker-compose configuration.

Fixes:
- Corrected KeyCloak import.
- Restored missing getEducation function.
- Ensured proper HTTP redirects.
- Fixed PKCE code verifier length.
This commit is contained in:
Tuan-Dat Tran
2025-11-21 20:28:56 +01:00
parent 88aeaa9002
commit 1346d36f5d
14 changed files with 704 additions and 78 deletions

View File

@@ -29,6 +29,19 @@ interface Experience extends ExperienceTranslation {
company_url: string | null;
}
interface EducationTranslation {
institution: string;
degree: string;
description: string | null;
}
interface Education extends EducationTranslation {
start_date: string;
end_date: string | null;
institution_url: string | null;
display_order: number;
}
interface SkillTranslation {
name: string;
category_display: string | null;
@@ -63,6 +76,18 @@ export function getExperience(lang: string): Experience[] {
return experience;
}
export function getEducation(lang: string): Education[] {
const education = db.query(`
SELECT e.start_date, e.end_date, e.institution_url, e.display_order,
et.institution, et.degree, et.description
FROM education e
JOIN education_translations et ON e.id = et.education_id
WHERE et.language_code = $lang
ORDER BY e.display_order ASC, e.start_date DESC
`).all({ $lang: lang }) as Education[];
return education;
}
export function getSkills(lang: string): Skill[] {
const skills = db.query(`
SELECT s.category, s.icon, s.display_order,
@@ -75,44 +100,55 @@ export function getSkills(lang: string): Skill[] {
return skills;
}
export function getEducation(lang: string): Education[] {
const education = db.query(`
SELECT e.start_date, e.end_date, e.institution_url, e.display_order,
et.institution, et.degree, et.description
FROM education e
JOIN education_translations et ON e.id = et.education_id
WHERE et.language_code = $lang
ORDER BY e.display_order ASC, e.start_date DESC
`).all({ $lang: lang }) as Education[];
return education;
}
export function getAllData(lang: string) {
return {
profile: getProfile(lang),
experience: getExperience(lang),
education: getEducation(lang),
skills: getSkills(lang),
};
}
// --- Admin Queries ---
export function getAdminProfile() {
const profile = db.query(`SELECT * FROM profile WHERE id = 1`).get() as any;
const translations = db.query(`SELECT * FROM profile_translations WHERE profile_id = 1`).all() as any[];
translations.forEach(t => {
profile[`name_${t.language_code}`] = t.name;
profile[`job_title_${t.language_code}`] = t.job_title;
profile[`summary_${t.language_code}`] = t.summary;
profile[`location_${t.language_code}`] = t.location;
});
return profile;
}
export function getAdminExperience() {
const exps = db.query(`SELECT * FROM experience ORDER BY display_order ASC, start_date DESC`).all() as any[];
return exps.map(e => {
const trans = db.query(`SELECT * FROM experience_translations WHERE experience_id = $id`, { $id: e.id }).all() as any[];
trans.forEach(t => {
e[`company_name_${t.language_code}`] = t.company_name;
e[`role_${t.language_code}`] = t.role;
e[`description_${t.language_code}`] = t.description;
e[`location_${t.language_code}`] = t.location;
});
return e;
});
}
export function getAdminEducation() {
const edus = db.query(`SELECT * FROM education ORDER BY display_order ASC, start_date DESC`).all() as any[];
return edus.map(e => {
const trans = db.query(`SELECT * FROM education_translations WHERE education_id = $id`, { $id: e.id }).all() as any[];
trans.forEach(t => {
e[`institution_${t.language_code}`] = t.institution;
e[`degree_${t.language_code}`] = t.degree;
e[`description_${t.language_code}`] = t.description;
});
return e;
});
}