mirror of
https://github.com/PeterMaquiran/tvone.git
synced 2026-04-23 12:35:51 +00:00
add auth moodule
This commit is contained in:
@@ -2,9 +2,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { ThumbsUp, MessageCircle, Send, Share2, LinkIcon, Link2, ChevronRight } from 'lucide-react';
|
import { ThumbsUp, MessageCircle, Send, Share2, LinkIcon, Link2, ChevronRight } from 'lucide-react';
|
||||||
import { TvoneAdBanner, TvoneFooter } from '../components/tvone-content';
|
import { TvoneAdBanner, TvoneFooter } from '../../components/tvone-content';
|
||||||
import { TvonePromoStrip } from '../components/tvone-promo-strip';
|
import { TvonePromoStrip } from '../../components/tvone-promo-strip';
|
||||||
import { TvoneSiteNav } from '../components/tvone-site-nav';
|
import { TvoneSiteNav } from '../../components/tvone-site-nav';
|
||||||
import { SiWhatsapp, SiFacebook, SiX } from 'react-icons/si'; // Ícones oficiais de marca
|
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 { FiCopy } from 'react-icons/fi'; // Ícone de cópia mais limpo
|
||||||
import Link from 'next/link';
|
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