diff --git a/app/category/page.tsx b/app/(page)/category/page.tsx similarity index 99% rename from app/category/page.tsx rename to app/(page)/category/page.tsx index cd48eae..22f6304 100644 --- a/app/category/page.tsx +++ b/app/(page)/category/page.tsx @@ -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'; diff --git a/app/panel/page.tsx b/app/(page)/panel/page.tsx similarity index 100% rename from app/panel/page.tsx rename to app/(page)/panel/page.tsx diff --git a/app/api/slider-photos/route.ts b/app/api/slider-photos/route.ts deleted file mode 100644 index 164ab16..0000000 --- a/app/api/slider-photos/route.ts +++ /dev/null @@ -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([]); -// } -// } diff --git a/components/CategoryModal.tsx b/app/components/CategoryModal.tsx similarity index 100% rename from components/CategoryModal.tsx rename to app/components/CategoryModal.tsx diff --git a/components/CategoryTree.tsx b/app/components/CategoryTree.tsx similarity index 100% rename from components/CategoryTree.tsx rename to app/components/CategoryTree.tsx diff --git a/app/feature/auth/keycloak-config.ts b/app/feature/auth/keycloak-config.ts new file mode 100644 index 0000000..6dc0824 --- /dev/null +++ b/app/feature/auth/keycloak-config.ts @@ -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."); + } \ No newline at end of file diff --git a/app/feature/auth/session-mapper.ts b/app/feature/auth/session-mapper.ts new file mode 100644 index 0000000..fa6f039 --- /dev/null +++ b/app/feature/auth/session-mapper.ts @@ -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, + }; +}; \ No newline at end of file diff --git a/app/feature/auth/token-refresher.ts b/app/feature/auth/token-refresher.ts new file mode 100644 index 0000000..3c563f2 --- /dev/null +++ b/app/feature/auth/token-refresher.ts @@ -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', + }; + } +}; \ No newline at end of file diff --git a/app/types/auth.types.tsx b/app/types/auth.types.tsx new file mode 100644 index 0000000..d73bbec --- /dev/null +++ b/app/types/auth.types.tsx @@ -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'; \ No newline at end of file diff --git a/hooks/useCategories.ts b/hooks/useCategories.ts deleted file mode 100644 index f2b25e7..0000000 --- a/hooks/useCategories.ts +++ /dev/null @@ -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([]); - const [flat, setFlat] = useState([]); - 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, - }; -} \ No newline at end of file