mirror of
https://github.com/PeterMaquiran/tvone.git
synced 2026-04-23 04:25:51 +00:00
add auth moodule
This commit is contained in:
@@ -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';
|
||||
@@ -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([]);
|
||||
// }
|
||||
// }
|
||||
@@ -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.");
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user