add auth moodule

This commit is contained in:
2026-04-18 13:43:13 +01:00
parent f163f22987
commit ba17904895
10 changed files with 131 additions and 106 deletions
@@ -2,9 +2,9 @@
import React, { useState } from 'react';
import Image from 'next/image';
import { ThumbsUp, MessageCircle, Send, Share2, LinkIcon, Link2, ChevronRight } from 'lucide-react';
import { TvoneAdBanner, TvoneFooter } from '../components/tvone-content';
import { TvonePromoStrip } from '../components/tvone-promo-strip';
import { TvoneSiteNav } from '../components/tvone-site-nav';
import { TvoneAdBanner, TvoneFooter } from '../../components/tvone-content';
import { TvonePromoStrip } from '../../components/tvone-promo-strip';
import { TvoneSiteNav } from '../../components/tvone-site-nav';
import { SiWhatsapp, SiFacebook, SiX } from 'react-icons/si'; // Ícones oficiais de marca
import { FiCopy } from 'react-icons/fi'; // Ícone de cópia mais limpo
import Link from 'next/link';
-14
View File
@@ -1,14 +0,0 @@
// import { NextResponse } from "next/server";
// import { readSliderPhotos } from "@/lib/slider-photos";
// export const runtime = "nodejs";
// export const dynamic = "force-dynamic";
// export async function GET() {
// try {
// const photos = readSliderPhotos();
// return NextResponse.json(photos);
// } catch {
// return NextResponse.json([]);
// }
// }
+21
View File
@@ -0,0 +1,21 @@
/**
* KEYCLOAK CONFIGURATION
* Logic: Environment variable validation and OIDC configuration.
*/
export const keycloakConfig = {
clientId: process.env.KEYCLOAK_CLIENT_ID!,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
issuer: `${process.env.KEYCLOAK_ISSUER_URL}/realms/${process.env.KEYCLOAK_REALM}`,
// Scopes needed for OIDC and profile access
scope: 'openid profile email',
// Endpoint for global logout
endSessionEndpoint: `${process.env.KEYCLOAK_ISSUER_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/logout`,
};
// Simple check to ensure environment variables are present
if (!process.env.KEYCLOAK_CLIENT_ID || !process.env.KEYCLOAK_ISSUER_URL) {
console.warn("Auth Warning: Keycloak environment variables are missing.");
}
+21
View File
@@ -0,0 +1,21 @@
/**
* SESSION MAPPER
* Logic: Data transformation (JWT -> Clean Profile).
* Purpose: Prevents leaking sensitive JWT metadata to the UI layer.
*/
import { UserProfile, RawKeycloakToken } from '../../types/auth.types';
export const mapKeycloakProfile = (token: RawKeycloakToken): UserProfile => {
return {
id: token.sub,
name: token.name || 'Guest User',
email: token.email,
username: token.preferred_username,
// Extracting roles for domain-specific logic (e.g., Editor, Admin)
roles: token.realm_access?.roles || [],
avatar: token.picture || null,
// Custom logic to check for premium status
isPremium: token.realm_access?.roles.includes('premium_subscriber') ?? false,
};
};
+41
View File
@@ -0,0 +1,41 @@
/**
* TOKEN REFRESHER
* Logic: Silent background token rotation.
* Role: Communicates with Keycloak to exchange a Refresh Token for a new Access Token.
*/
import { keycloakConfig } from './keycloak-config';
export const refreshAccessToken = async (token: any) => {
try {
const url = `${process.env.KEYCLOAK_ISSUER_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`;
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: keycloakConfig.clientId,
client_secret: keycloakConfig.clientSecret,
grant_type: 'refresh_token',
refresh_token: token.refreshToken,
}),
});
const refreshedTokens = await response.json();
if (!response.ok) throw refreshedTokens;
return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, // Fallback to old refresh token
};
} catch (error) {
console.error('Error refreshing access token', error);
return {
...token,
error: 'RefreshAccessTokenError',
};
}
};
+45
View File
@@ -0,0 +1,45 @@
/**
* RAW KEYCLOAK TOKEN
* Data Layer: Represents the uncleaned decoded JWT payload from Keycloak.
*/
export interface RawKeycloakToken {
sub: string; // Unique User ID
name?: string;
email?: string;
preferred_username: string;
given_name?: string;
family_name?: string;
email_verified: boolean;
picture?: string; // Avatar URL from Keycloak
realm_access?: {
roles: string[]; // Global roles (e.g., 'admin', 'editor')
};
resource_access?: {
[key: string]: {
roles: string[]; // Client-specific roles
};
};
exp: number; // Expiration Timestamp
iat: number; // Issued At Timestamp
}
/**
* USER PROFILE
* UI Layer: Represents the "Cleaned" object used by the frontend.
* Logic: Extracted from RawKeycloakToken via session-mapper.ts.
*/
export interface UserProfile {
id: string;
name: string;
email?: string;
username: string;
avatar: string | null;
roles: string[];
isPremium: boolean; // Derived state for the News Portal
}
/**
* AUTH STATUS
* Logic: Standardizes the possible states of the session.
*/
export type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';
-89
View File
@@ -1,89 +0,0 @@
import { useEffect, useState } from "react";
import {
Category,
getCategoriesTree,
getCategoriesFlat,
createCategory,
updateCategory,
deleteCategory,
} from "@/lib/categories.api";
import { slugify } from "@/lib/slug";
export function useCategories() {
const [tree, setTree] = useState<Category[]>([]);
const [flat, setFlat] = useState<Category[]>([]);
const [loading, setLoading] = useState(false);
const [form, setForm] = useState({
id: null as string | null,
name: "",
slug: "",
parentId: null as string | null,
});
async function load() {
setLoading(true);
try {
const [t, f] = await Promise.all([
getCategoriesTree(),
getCategoriesFlat(),
]);
setTree(t);
setFlat(f);
} finally {
setLoading(false);
}
}
useEffect(() => {
load();
}, []);
async function save() {
const payload = {
name: form.name,
slug: form.slug || slugify(form.name),
parentId: form.parentId,
};
if (form.id) {
await updateCategory(form.id, payload);
} else {
await createCategory(payload);
}
resetForm();
load();
}
async function remove(id: string) {
await deleteCategory(id);
load();
}
function edit(cat: Category) {
setForm({
id: cat.id,
name: cat.name,
slug: cat.slug,
parentId: cat.parentId || null,
});
}
function resetForm() {
setForm({ id: null, name: "", slug: "", parentId: null });
}
return {
tree,
flat,
form,
setForm,
save,
remove,
edit,
resetForm,
loading,
};
}