try refactor auth

This commit is contained in:
overcuriousity 2025-07-24 00:26:01 +02:00
parent 0ac31484d5
commit 32fca8a06f
10 changed files with 236 additions and 135 deletions

View File

@ -14,5 +14,6 @@ export default defineConfig({
server: {
port: 4321,
host: true
}
},
allowImportingTsExtensions: true
});

View File

@ -150,24 +150,12 @@ const sortedTags = Object.entries(tagFrequency)
let isTagCloudExpanded = false;
// Check authentication status and show/hide AI button
async function checkAuthAndShowAIButton() {
try {
const response = await fetch('/api/auth/status');
const data = await response.json();
// Show AI button if authentication is not required OR if user is authenticated
if (!data.authRequired || data.authenticated) {
if (aiViewToggle) {
aiViewToggle.style.display = 'inline-flex';
}
}
} catch (error) {
console.log('Auth check failed, AI button remains hidden');
}
async function initAIButton() {
await showIfAuthenticated('#ai-view-toggle');
}
// Call auth check on page load
checkAuthAndShowAIButton();
initAIButton();
// Initialize tag cloud state
function initTagCloud() {

View File

@ -1,6 +1,6 @@
// src/pages/api/ai/query.ts
import type { APIRoute } from 'astro';
import { getSessionFromRequest, verifySession } from '../../../utils/auth.js';
import { withAPIAuth, createAuthErrorResponse } from '../../../utils/auth.js';
import { getCompressedToolsDataForAI } from '../../../utils/dataService.js';
export const prerender = false;
@ -275,30 +275,13 @@ Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des J
export const POST: APIRoute = async ({ request }) => {
try {
// Check if authentication is required
const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
let userId = 'test-user';
if (authRequired) {
// Authentication check
const sessionToken = getSessionFromRequest(request);
if (!sessionToken) {
return new Response(JSON.stringify({ error: 'Authentication required' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
// CONSOLIDATED: Replace 20+ lines with single function call
const authResult = await withAPIAuth(request);
if (!authResult.authenticated) {
return createAuthErrorResponse();
}
const session = await verifySession(sessionToken);
if (!session) {
return new Response(JSON.stringify({ error: 'Invalid session' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
userId = session.userId;
}
const userId = authResult.userId;
// Rate limiting
if (!checkRateLimit(userId)) {

View File

@ -1,30 +1,17 @@
---
// src/pages/contribute/index.astro - Updated for Phase 3
// src/pages/contribute/index.astro - Consolidated Auth
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getSessionFromRequest, verifySession } from '../../utils/auth.js';
import { getAuthContext, requireAuth } from '../../utils/serverAuth.js';
import { withAuth } from '../../utils/auth.js'; // Note: .js extension!
export const prerender = false;
// Check authentication
const authRequired = import.meta.env.AUTHENTICATION_NECESSARY !== 'false';
let isAuthenticated = false;
let userEmail = '';
if (authRequired) {
const sessionToken = getSessionFromRequest(Astro.request);
if (sessionToken) {
const session = await verifySession(sessionToken);
if (session) {
isAuthenticated = true;
userEmail = session.email;
}
}
const authContext = await getAuthContext(Astro);
const authRedirect = requireAuth(authContext, Astro.url.toString());
if (authRedirect) return authRedirect;
// CONSOLIDATED: Replace 15+ lines with single function call
const authResult = await withAuth(Astro);
if (authResult instanceof Response) {
return authResult; // Redirect to login
}
const { authenticated, userEmail, userId } = authResult;
---
<BaseLayout title="Contribute" description="Contribute tools, methods, concepts, and knowledge articles to CC24-Guide">

View File

@ -1,32 +1,19 @@
---
// src/pages/contribute/knowledgebase.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getSessionFromRequest, verifySession } from '../../utils/auth.js';
import { getAuthContext, requireAuth } from '../../utils/serverAuth.js';
import { withAuth } from '../../utils/auth.js';
import { getToolsData } from '../../utils/dataService.js';
export const prerender = false;
// Check authentication
const authRequired = import.meta.env.AUTHENTICATION_NECESSARY !== 'false';
let isAuthenticated = false;
let userEmail = '';
if (authRequired) {
const sessionToken = getSessionFromRequest(Astro.request);
if (sessionToken) {
const session = await verifySession(sessionToken);
if (session) {
isAuthenticated = true;
userEmail = session.email;
}
}
const authContext = await getAuthContext(Astro);
const authRedirect = requireAuth(authContext, Astro.url.toString());
if (authRedirect) return authRedirect;
const authResult = await withAuth(Astro);
if (authResult instanceof Response) {
return authResult; // Redirect to login
}
const { authenticated, userEmail, userId } = authResult;
const data = await getToolsData();
const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.name));
---

View File

@ -1,13 +1,16 @@
---
// src/pages/contribute/tool.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getAuthContext, requireAuth } from '../../utils/serverAuth.js';
import { withAuth } from '../../utils/auth.js';
import { getToolsData } from '../../utils/dataService.js';
// Check authentication
const authContext = await getAuthContext(Astro);
const authRedirect = requireAuth(authContext, Astro.url.toString());
if (authRedirect) return authRedirect;
const authResult = await withAuth(Astro);
if (authResult instanceof Response) {
return authResult; // Redirect to login
}
const { authenticated, userEmail, userId } = authResult;
// Load existing data for validation and editing
const data = await getToolsData();

View File

@ -120,6 +120,7 @@ const tools = data.tools;
}
}
import { requireClientAuth } from '../utils/auth.js';
// Handle view changes and filtering
document.addEventListener('DOMContentLoaded', () => {
const toolsContainer = document.getElementById('tools-container') as HTMLElement;
@ -177,17 +178,11 @@ const tools = data.tools;
}
}
// AI Query Button Handler
if (aiQueryBtn) {
aiQueryBtn.addEventListener('click', async () => {
const authStatus = await checkAuthentication();
if (authStatus.authRequired && !authStatus.authenticated) {
const returnUrl = `${window.location.pathname}?view=ai`;
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(returnUrl)}`;
} else {
switchToView('ai');
}
await requireClientAuth(() => switchToView('ai'), `${window.location.pathname}?view=ai`);
});
}

View File

@ -2,6 +2,8 @@
import { SignJWT, jwtVerify, type JWTPayload } from 'jose';
import { serialize, parse } from 'cookie';
import { config } from 'dotenv';
import type { AstroGlobal, APIRoute } from 'astro';
// Load environment variables
config();
@ -197,3 +199,143 @@ export function getUserEmail(userInfo: UserInfo): string {
return userInfo.email ||
`${userInfo.preferred_username || userInfo.sub || 'unknown'}@cc24.dev`;
}
// === CONSOLIDATION: Server-side Auth Helpers ===
export interface AuthContext {
authenticated: boolean;
session: SessionData | null;
userEmail: string;
userId: string;
}
/**
* Consolidated auth check for Astro pages
* Replaces repeated auth patterns in contribute pages
*/
export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Response> {
const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
// If auth not required, return mock context
if (!authRequired) {
return {
authenticated: true,
session: null,
userEmail: 'anonymous@example.com',
userId: 'anonymous'
};
}
// Check session
const sessionToken = getSessionFromRequest(Astro.request);
if (!sessionToken) {
const loginUrl = `/api/auth/login?returnTo=${encodeURIComponent(Astro.url.toString())}`;
return new Response(null, {
status: 302,
headers: { 'Location': loginUrl }
});
}
const session = await verifySession(sessionToken);
if (!session) {
const loginUrl = `/api/auth/login?returnTo=${encodeURIComponent(Astro.url.toString())}`;
return new Response(null, {
status: 302,
headers: { 'Location': loginUrl }
});
}
return {
authenticated: true,
session,
userEmail: session.email,
userId: session.userId
};
}
/**
* Consolidated auth check for API endpoints
* Replaces repeated auth patterns in API routes
*/
export async function withAPIAuth(request: Request): Promise<{ authenticated: boolean; userId: string; session?: SessionData }> {
const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
if (!authRequired) {
return {
authenticated: true,
userId: 'anonymous'
};
}
const sessionToken = getSessionFromRequest(request);
if (!sessionToken) {
return { authenticated: false, userId: '' };
}
const session = await verifySession(sessionToken);
if (!session) {
return { authenticated: false, userId: '' };
}
return {
authenticated: true,
userId: session.userId,
session
};
}
/**
* Helper to create consistent API auth error responses
*/
export function createAuthErrorResponse(message: string = 'Authentication required'): Response {
return new Response(JSON.stringify({ error: message }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
async function checkClientAuth() {
try {
const response = await fetch('/api/auth/status');
const data = await response.json();
return {
authenticated: data.authenticated,
authRequired: data.authRequired,
expires: data.expires
};
} catch (error) {
console.error('Auth check failed:', error);
return {
authenticated: false,
authRequired: true
};
}
}
/**
* Redirect to login if not authenticated, otherwise execute callback
*/
export async function requireClientAuth(callback, returnUrl) {
const authStatus = await checkClientAuth();
if (authStatus.authRequired && !authStatus.authenticated) {
const targetUrl = returnUrl || window.location.href;
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(targetUrl)}`;
} else {
callback();
}
}
/**
* Show/hide element based on authentication
*/
export async function showIfAuthenticated(selector) {
const authStatus = await checkClientAuth();
const element = document.querySelector(selector);
if (element) {
element.style.display = (!authStatus.authRequired || authStatus.authenticated)
? 'inline-flex'
: 'none';
}
}

55
src/utils/client-auth.ts Normal file
View File

@ -0,0 +1,55 @@
// src/scripts/client-auth.js - Client-side auth utilities
/**
* Consolidated client-side auth status check
*/
async function checkClientAuth() {
try {
const response = await fetch('/api/auth/status');
const data = await response.json();
return {
authenticated: data.authenticated,
authRequired: data.authRequired,
expires: data.expires
};
} catch (error) {
console.error('Auth check failed:', error);
return {
authenticated: false,
authRequired: true
};
}
}
/**
* Redirect to login if not authenticated, otherwise execute callback
*/
async function requireClientAuth(callback, returnUrl) {
const authStatus = await checkClientAuth();
if (authStatus.authRequired && !authStatus.authenticated) {
const targetUrl = returnUrl || window.location.href;
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(targetUrl)}`;
} else {
callback();
}
}
/**
* Show/hide element based on authentication
*/
async function showIfAuthenticated(selector) {
const authStatus = await checkClientAuth();
const element = document.querySelector(selector);
if (element) {
element.style.display = (!authStatus.authRequired || authStatus.authenticated)
? 'inline-flex'
: 'none';
}
}
// Make functions available globally
window.checkClientAuth = checkClientAuth;
window.requireClientAuth = requireClientAuth;
window.showIfAuthenticated = showIfAuthenticated;

View File

@ -1,40 +0,0 @@
import type { AstroGlobal } from 'astro';
import { getSessionFromRequest, verifySession, type SessionData } from './auth.js';
export interface AuthContext {
authenticated: boolean;
session: SessionData | null;
}
// Check authentication status for server-side pages
export async function getAuthContext(Astro: AstroGlobal): Promise<AuthContext> {
try {
const sessionToken = getSessionFromRequest(Astro.request);
if (!sessionToken) {
return { authenticated: false, session: null };
}
const session = await verifySession(sessionToken);
return {
authenticated: session !== null,
session
};
} catch (error) {
console.error('Failed to get auth context:', error);
return { authenticated: false, session: null };
}
}
// Redirect to login if not authenticated
export function requireAuth(authContext: AuthContext, currentUrl: string): Response | null {
if (!authContext.authenticated) {
const loginUrl = `/api/auth/login?returnTo=${encodeURIComponent(currentUrl)}`;
return new Response(null, {
status: 302,
headers: { 'Location': loginUrl }
});
}
return null;
}