try refactor auth
This commit is contained in:
parent
0ac31484d5
commit
32fca8a06f
@ -14,5 +14,6 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 4321,
|
port: 4321,
|
||||||
host: true
|
host: true
|
||||||
}
|
},
|
||||||
});
|
allowImportingTsExtensions: true
|
||||||
|
});
|
||||||
|
@ -150,24 +150,12 @@ const sortedTags = Object.entries(tagFrequency)
|
|||||||
let isTagCloudExpanded = false;
|
let isTagCloudExpanded = false;
|
||||||
|
|
||||||
// Check authentication status and show/hide AI button
|
// Check authentication status and show/hide AI button
|
||||||
async function checkAuthAndShowAIButton() {
|
async function initAIButton() {
|
||||||
try {
|
await showIfAuthenticated('#ai-view-toggle');
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call auth check on page load
|
// Call auth check on page load
|
||||||
checkAuthAndShowAIButton();
|
initAIButton();
|
||||||
|
|
||||||
// Initialize tag cloud state
|
// Initialize tag cloud state
|
||||||
function initTagCloud() {
|
function initTagCloud() {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// src/pages/api/ai/query.ts
|
// src/pages/api/ai/query.ts
|
||||||
import type { APIRoute } from 'astro';
|
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';
|
import { getCompressedToolsDataForAI } from '../../../utils/dataService.js';
|
||||||
|
|
||||||
export const prerender = false;
|
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 }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
// Check if authentication is required
|
// CONSOLIDATED: Replace 20+ lines with single function call
|
||||||
const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
|
const authResult = await withAPIAuth(request);
|
||||||
let userId = 'test-user';
|
if (!authResult.authenticated) {
|
||||||
|
return createAuthErrorResponse();
|
||||||
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' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
// Rate limiting
|
||||||
if (!checkRateLimit(userId)) {
|
if (!checkRateLimit(userId)) {
|
||||||
|
@ -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 BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
import { getSessionFromRequest, verifySession } from '../../utils/auth.js';
|
import { withAuth } from '../../utils/auth.js'; // Note: .js extension!
|
||||||
import { getAuthContext, requireAuth } from '../../utils/serverAuth.js';
|
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
// Check authentication
|
// CONSOLIDATED: Replace 15+ lines with single function call
|
||||||
const authRequired = import.meta.env.AUTHENTICATION_NECESSARY !== 'false';
|
const authResult = await withAuth(Astro);
|
||||||
let isAuthenticated = false;
|
if (authResult instanceof Response) {
|
||||||
let userEmail = '';
|
return authResult; // Redirect to login
|
||||||
|
|
||||||
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 { authenticated, userEmail, userId } = authResult;
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="Contribute" description="Contribute tools, methods, concepts, and knowledge articles to CC24-Guide">
|
<BaseLayout title="Contribute" description="Contribute tools, methods, concepts, and knowledge articles to CC24-Guide">
|
||||||
|
@ -1,32 +1,19 @@
|
|||||||
---
|
---
|
||||||
// src/pages/contribute/knowledgebase.astro
|
// src/pages/contribute/knowledgebase.astro
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
import { getSessionFromRequest, verifySession } from '../../utils/auth.js';
|
import { withAuth } from '../../utils/auth.js';
|
||||||
import { getAuthContext, requireAuth } from '../../utils/serverAuth.js';
|
|
||||||
import { getToolsData } from '../../utils/dataService.js';
|
import { getToolsData } from '../../utils/dataService.js';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
// Check authentication
|
// Check authentication
|
||||||
const authRequired = import.meta.env.AUTHENTICATION_NECESSARY !== 'false';
|
const authResult = await withAuth(Astro);
|
||||||
let isAuthenticated = false;
|
if (authResult instanceof Response) {
|
||||||
let userEmail = '';
|
return authResult; // Redirect to login
|
||||||
|
|
||||||
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 { authenticated, userEmail, userId } = authResult;
|
||||||
|
|
||||||
const data = await getToolsData();
|
const data = await getToolsData();
|
||||||
const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||||
---
|
---
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
---
|
---
|
||||||
// src/pages/contribute/tool.astro
|
// src/pages/contribute/tool.astro
|
||||||
import BaseLayout from '../../layouts/BaseLayout.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';
|
import { getToolsData } from '../../utils/dataService.js';
|
||||||
|
|
||||||
// Check authentication
|
// Check authentication
|
||||||
const authContext = await getAuthContext(Astro);
|
const authResult = await withAuth(Astro);
|
||||||
const authRedirect = requireAuth(authContext, Astro.url.toString());
|
if (authResult instanceof Response) {
|
||||||
if (authRedirect) return authRedirect;
|
return authResult; // Redirect to login
|
||||||
|
}
|
||||||
|
|
||||||
|
const { authenticated, userEmail, userId } = authResult;
|
||||||
|
|
||||||
// Load existing data for validation and editing
|
// Load existing data for validation and editing
|
||||||
const data = await getToolsData();
|
const data = await getToolsData();
|
||||||
|
@ -119,7 +119,8 @@ const tools = data.tools;
|
|||||||
navigateToMatrix: (toolName: string) => void;
|
navigateToMatrix: (toolName: string) => void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { requireClientAuth } from '../utils/auth.js';
|
||||||
// Handle view changes and filtering
|
// Handle view changes and filtering
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const toolsContainer = document.getElementById('tools-container') as HTMLElement;
|
const toolsContainer = document.getElementById('tools-container') as HTMLElement;
|
||||||
@ -177,17 +178,11 @@ const tools = data.tools;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// AI Query Button Handler
|
// AI Query Button Handler
|
||||||
if (aiQueryBtn) {
|
if (aiQueryBtn) {
|
||||||
aiQueryBtn.addEventListener('click', async () => {
|
aiQueryBtn.addEventListener('click', async () => {
|
||||||
const authStatus = await checkAuthentication();
|
await requireClientAuth(() => switchToView('ai'), `${window.location.pathname}?view=ai`);
|
||||||
|
|
||||||
if (authStatus.authRequired && !authStatus.authenticated) {
|
|
||||||
const returnUrl = `${window.location.pathname}?view=ai`;
|
|
||||||
window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(returnUrl)}`;
|
|
||||||
} else {
|
|
||||||
switchToView('ai');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
import { SignJWT, jwtVerify, type JWTPayload } from 'jose';
|
import { SignJWT, jwtVerify, type JWTPayload } from 'jose';
|
||||||
import { serialize, parse } from 'cookie';
|
import { serialize, parse } from 'cookie';
|
||||||
import { config } from 'dotenv';
|
import { config } from 'dotenv';
|
||||||
|
import type { AstroGlobal, APIRoute } from 'astro';
|
||||||
|
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
config();
|
config();
|
||||||
@ -196,4 +198,144 @@ export function logAuthEvent(event: string, details?: any) {
|
|||||||
export function getUserEmail(userInfo: UserInfo): string {
|
export function getUserEmail(userInfo: UserInfo): string {
|
||||||
return userInfo.email ||
|
return userInfo.email ||
|
||||||
`${userInfo.preferred_username || userInfo.sub || 'unknown'}@cc24.dev`;
|
`${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
55
src/utils/client-auth.ts
Normal 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;
|
@ -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;
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user