auth splitting
This commit is contained in:
		
							parent
							
								
									a4f4e03cba
								
							
						
					
					
						commit
						d2fdeccce3
					
				@ -7,7 +7,10 @@ AUTH_SECRET=change-this-to-a-strong-secret-key-in-production
 | 
			
		||||
OIDC_ENDPOINT=https://your-oidc-provider.com
 | 
			
		||||
OIDC_CLIENT_ID=your-oidc-client-id
 | 
			
		||||
OIDC_CLIENT_SECRET=your-oidc-client-secret
 | 
			
		||||
AUTHENTICATION_NECESSARY=true
 | 
			
		||||
 | 
			
		||||
# Auth Scopes - set to true in prod
 | 
			
		||||
AUTHENTICATION_NECESSARY_CONTRIBUTIONS=true
 | 
			
		||||
AUTHENTICATION_NECESSARY_AI=true
 | 
			
		||||
 | 
			
		||||
# Application Configuration (Required)
 | 
			
		||||
PUBLIC_BASE_URL=https://your-domain.com
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,7 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
 | 
			
		||||
            <line x1="12" y1="8" x2="12" y2="12"/>
 | 
			
		||||
            <line x1="12" y1="16" x2="12.01" y2="16"/>
 | 
			
		||||
          </svg>
 | 
			
		||||
          Ihre Anfrage wird an mistral.ai übertragen und unterliegt deren 
 | 
			
		||||
          Ihre Anfrage wird über die kostenlose API von mistral.ai übertragen, wird für KI-Training verwendet und unterliegt deren 
 | 
			
		||||
          <a href="https://mistral.ai/privacy-policy/" target="_blank" rel="noopener noreferrer" style="color: var(--color-primary); text-decoration: underline;">Datenschutzrichtlinien</a>
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@ -112,15 +112,31 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
 | 
			
		||||
      (window as any).isToolHosted = isToolHosted;
 | 
			
		||||
 | 
			
		||||
      // Client-side auth functions (consolidated from client-auth.js)
 | 
			
		||||
      async function checkClientAuth() {
 | 
			
		||||
      async function checkClientAuth(context = 'general') {
 | 
			
		||||
        try {
 | 
			
		||||
          const response = await fetch('/api/auth/status');
 | 
			
		||||
          const data = await response.json();
 | 
			
		||||
          return {
 | 
			
		||||
            authenticated: data.authenticated,
 | 
			
		||||
            authRequired: data.authRequired,
 | 
			
		||||
            expires: data.expires
 | 
			
		||||
          };
 | 
			
		||||
          
 | 
			
		||||
          switch (context) {
 | 
			
		||||
            case 'contributions':
 | 
			
		||||
              return {
 | 
			
		||||
                authenticated: data.contributionAuthenticated,
 | 
			
		||||
                authRequired: data.contributionAuthRequired,
 | 
			
		||||
                expires: data.expires
 | 
			
		||||
              };
 | 
			
		||||
            case 'ai':
 | 
			
		||||
              return {
 | 
			
		||||
                authenticated: data.aiAuthenticated,
 | 
			
		||||
                authRequired: data.aiAuthRequired,
 | 
			
		||||
                expires: data.expires
 | 
			
		||||
              };
 | 
			
		||||
            default:
 | 
			
		||||
              return {
 | 
			
		||||
                authenticated: data.authenticated,
 | 
			
		||||
                authRequired: data.contributionAuthRequired || data.aiAuthRequired,
 | 
			
		||||
                expires: data.expires
 | 
			
		||||
              };
 | 
			
		||||
          }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          console.error('Auth check failed:', error);
 | 
			
		||||
          return {
 | 
			
		||||
@ -130,8 +146,8 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      async function requireClientAuth(callback, returnUrl) {
 | 
			
		||||
        const authStatus = await checkClientAuth();
 | 
			
		||||
      async function requireClientAuth(callback, returnUrl, context = 'general') {
 | 
			
		||||
        const authStatus = await checkClientAuth(context);
 | 
			
		||||
        
 | 
			
		||||
        if (authStatus.authRequired && !authStatus.authenticated) {
 | 
			
		||||
          const targetUrl = returnUrl || window.location.href;
 | 
			
		||||
@ -145,8 +161,8 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      async function showIfAuthenticated(selector) {
 | 
			
		||||
        const authStatus = await checkClientAuth();
 | 
			
		||||
      async function showIfAuthenticated(selector, context = 'general') {
 | 
			
		||||
        const authStatus = await checkClientAuth(context);
 | 
			
		||||
        const element = document.querySelector(selector);
 | 
			
		||||
        
 | 
			
		||||
        if (element) {
 | 
			
		||||
@ -157,11 +173,9 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function setupAuthButtons(selector = '[data-contribute-button]') {
 | 
			
		||||
        // Use event delegation on document for dynamic content support
 | 
			
		||||
        document.addEventListener('click', async (e) => {
 | 
			
		||||
          if (!e.target) return;
 | 
			
		||||
          
 | 
			
		||||
          // FIXED: Properly cast EventTarget to Element for closest() method
 | 
			
		||||
          const button = (e.target as Element).closest(selector);
 | 
			
		||||
          if (!button) return;
 | 
			
		||||
          
 | 
			
		||||
@ -169,10 +183,11 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
 | 
			
		||||
          
 | 
			
		||||
          console.log('[AUTH] Contribute button clicked:', button.getAttribute('data-contribute-button'));
 | 
			
		||||
          
 | 
			
		||||
          // ENHANCED: Use contributions context
 | 
			
		||||
          await requireClientAuth(() => {
 | 
			
		||||
            console.log('[AUTH] Navigation approved, redirecting to:', (button as HTMLAnchorElement).href);
 | 
			
		||||
            window.location.href = (button as HTMLAnchorElement).href;
 | 
			
		||||
          }, (button as HTMLAnchorElement).href);
 | 
			
		||||
          }, (button as HTMLAnchorElement).href, 'contributions');
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -187,7 +202,7 @@ const { title, description = 'CC24-Guide - A comprehensive directory of digital
 | 
			
		||||
      setupAuthButtons('[data-contribute-button]');
 | 
			
		||||
      
 | 
			
		||||
      const initAIButton = async () => {
 | 
			
		||||
        await showIfAuthenticated('#ai-view-toggle');
 | 
			
		||||
        await showIfAuthenticated('#ai-view-toggle', 'ai');
 | 
			
		||||
      };
 | 
			
		||||
      initAIButton();
 | 
			
		||||
      
 | 
			
		||||
 | 
			
		||||
@ -278,7 +278,7 @@ Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des J
 | 
			
		||||
export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
  try {
 | 
			
		||||
    // CONSOLIDATED: Replace 20+ lines with single function call (UNCHANGED)
 | 
			
		||||
    const authResult = await withAPIAuth(request);
 | 
			
		||||
    const authResult = await withAPIAuth(request, 'ai');
 | 
			
		||||
    if (!authResult.authenticated) {
 | 
			
		||||
      return createAuthErrorResponse();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
// src/pages/api/auth/status.ts (FIXED - Updated imports and consolidated)
 | 
			
		||||
// src/pages/api/auth/status.ts 
 | 
			
		||||
import type { APIRoute } from 'astro';
 | 
			
		||||
import { withAPIAuth } from '../../../utils/auth.js';
 | 
			
		||||
import { apiResponse, handleAPIRequest } from '../../../utils/api.js';
 | 
			
		||||
@ -7,14 +7,16 @@ export const prerender = false;
 | 
			
		||||
 | 
			
		||||
export const GET: APIRoute = async ({ request }) => {
 | 
			
		||||
  return await handleAPIRequest(async () => {
 | 
			
		||||
    // CONSOLIDATED: Single function call replaces 35+ lines
 | 
			
		||||
    const authResult = await withAPIAuth(request);
 | 
			
		||||
    const contributionAuth = await withAPIAuth(request, 'contributions');
 | 
			
		||||
    const aiAuth = await withAPIAuth(request, 'ai');
 | 
			
		||||
    
 | 
			
		||||
    return apiResponse.success({
 | 
			
		||||
      authenticated: authResult.authenticated,
 | 
			
		||||
      authRequired: authResult.authRequired,
 | 
			
		||||
      expires: authResult.session?.exp ? new Date(authResult.session.exp * 1000).toISOString() : null
 | 
			
		||||
      authenticated: contributionAuth.authenticated || aiAuth.authenticated,
 | 
			
		||||
      contributionAuthRequired: contributionAuth.authRequired,
 | 
			
		||||
      aiAuthRequired: aiAuth.authRequired,
 | 
			
		||||
      contributionAuthenticated: contributionAuth.authenticated,
 | 
			
		||||
      aiAuthenticated: aiAuth.authenticated,
 | 
			
		||||
      expires: contributionAuth.session?.exp ? new Date(contributionAuth.session.exp * 1000).toISOString() : null
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
  }, 'Status check failed');
 | 
			
		||||
};
 | 
			
		||||
@ -84,12 +84,12 @@ function validateKnowledgebaseData(data: KnowledgebaseContributionData): { valid
 | 
			
		||||
export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
  return await handleAPIRequest(async () => {
 | 
			
		||||
    // Check authentication
 | 
			
		||||
    const authResult = await withAPIAuth(request);
 | 
			
		||||
    const authResult = await withAPIAuth(request, 'contributions');
 | 
			
		||||
    if (authResult.authRequired && !authResult.authenticated) {
 | 
			
		||||
      return apiError.unauthorized();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const userEmail = authResult.session?.email || 'anonymous@example.com';
 | 
			
		||||
    const userEmail = authResult.session?.email || 'anon@anon.anon';
 | 
			
		||||
 | 
			
		||||
    // Rate limiting
 | 
			
		||||
    if (!checkRateLimit(userEmail)) {
 | 
			
		||||
 | 
			
		||||
@ -127,13 +127,13 @@ async function validateToolData(tool: any, action: string): Promise<{ valid: boo
 | 
			
		||||
export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
  return await handleAPIRequest(async () => {
 | 
			
		||||
    // Authentication check
 | 
			
		||||
    const authResult = await withAPIAuth(request);
 | 
			
		||||
    const authResult = await withAPIAuth(request, 'contributions');
 | 
			
		||||
    if (authResult.authRequired && !authResult.authenticated) {
 | 
			
		||||
      return apiError.unauthorized();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const userId = authResult.session?.userId || 'anonymous';
 | 
			
		||||
    const userEmail = authResult.session?.email || 'anonymous@example.com';
 | 
			
		||||
    const userEmail = authResult.session?.email || 'anon@anon.anon';
 | 
			
		||||
 | 
			
		||||
    // Rate limiting
 | 
			
		||||
    if (!checkRateLimit(userId)) {
 | 
			
		||||
 | 
			
		||||
@ -139,23 +139,19 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// POST endpoint for file uploads
 | 
			
		||||
export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
  return await handleAPIRequest(async () => {
 | 
			
		||||
    // Authentication check
 | 
			
		||||
    const authResult = await withAPIAuth(request);
 | 
			
		||||
    const authResult = await withAPIAuth(request, 'contributions');
 | 
			
		||||
    if (authResult.authRequired && !authResult.authenticated) {
 | 
			
		||||
      return apiError.unauthorized();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const userEmail = authResult.session?.email || 'anonymous@example.com';
 | 
			
		||||
    const userEmail = authResult.session?.email || 'anon@anon.anon';
 | 
			
		||||
 | 
			
		||||
    // Rate limiting
 | 
			
		||||
    if (!checkUploadRateLimit(userEmail)) {
 | 
			
		||||
      return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Parse multipart form data
 | 
			
		||||
    let formData;
 | 
			
		||||
    try {
 | 
			
		||||
      formData = await request.formData();
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import { withAuth } from '../../utils/auth.js'; // Note: .js extension!
 | 
			
		||||
export const prerender = false;
 | 
			
		||||
 | 
			
		||||
// CONSOLIDATED: Replace 15+ lines with single function call
 | 
			
		||||
const authResult = await withAuth(Astro);
 | 
			
		||||
const authResult = await withAuth(Astro, 'contributions');
 | 
			
		||||
if (authResult instanceof Response) {
 | 
			
		||||
  return authResult; // Redirect to login
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ import { getToolsData } from '../../utils/dataService.js';
 | 
			
		||||
export const prerender = false;
 | 
			
		||||
 | 
			
		||||
// Check authentication
 | 
			
		||||
const authResult = await withAuth(Astro);
 | 
			
		||||
const authResult = await withAuth(Astro, 'contributions');
 | 
			
		||||
if (authResult instanceof Response) {
 | 
			
		||||
  return authResult;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ import { getToolsData } from '../../utils/dataService.js';
 | 
			
		||||
export const prerender = false;
 | 
			
		||||
 | 
			
		||||
// Check authentication
 | 
			
		||||
const authResult = await withAuth(Astro);
 | 
			
		||||
const authResult = await withAuth(Astro, 'contributions');
 | 
			
		||||
if (authResult instanceof Response) {
 | 
			
		||||
  return authResult;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -121,15 +121,13 @@ const tools = data.tools;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // AI Query Button Handler using consolidated auth system
 | 
			
		||||
    if (aiQueryBtn) {
 | 
			
		||||
      aiQueryBtn.addEventListener('click', async () => {
 | 
			
		||||
        // Use the global auth system consistently
 | 
			
		||||
        if (typeof window.requireClientAuth === 'function') {
 | 
			
		||||
          await window.requireClientAuth(() => switchToView('ai'), `${window.location.pathname}?view=ai`);
 | 
			
		||||
          // ENHANCED: Use AI-specific authentication
 | 
			
		||||
          await window.requireClientAuth(() => switchToView('ai'), `${window.location.pathname}?view=ai`, 'ai');
 | 
			
		||||
        } else {
 | 
			
		||||
          // Better fallback logging
 | 
			
		||||
          console.warn('[AUTH] requireClientAuth not available - client-auth.js may not be loaded properly');
 | 
			
		||||
          console.warn('[AUTH] requireClientAuth not available');
 | 
			
		||||
          switchToView('ai');
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,8 @@ export interface AuthContext {
 | 
			
		||||
  userId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AuthContextType = 'contributions' | 'ai' | 'general';
 | 
			
		||||
 | 
			
		||||
export interface UserInfo {
 | 
			
		||||
  sub?: string;
 | 
			
		||||
  preferred_username?: string;
 | 
			
		||||
@ -265,10 +267,19 @@ export function verifyAuthState(request: Request, receivedState: string): {
 | 
			
		||||
  return { isValid: true, stateData };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * CONSOLIDATED: Create session with cookie headers
 | 
			
		||||
 * Replaces duplicated session creation in callback.ts and process.ts
 | 
			
		||||
 */
 | 
			
		||||
function getAuthRequirement(context: AuthContextType): boolean {
 | 
			
		||||
  switch (context) {
 | 
			
		||||
    case 'contributions':
 | 
			
		||||
      return process.env.AUTHENTICATION_NECESSARY_CONTRIBUTIONS !== 'false';
 | 
			
		||||
    case 'ai':
 | 
			
		||||
      return process.env.AUTHENTICATION_NECESSARY_AI !== 'false';
 | 
			
		||||
    case 'general':
 | 
			
		||||
      return process.env.AUTHENTICATION_NECESSARY !== 'false';
 | 
			
		||||
    default:
 | 
			
		||||
      return true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function createSessionWithCookie(userInfo: UserInfo): Promise<{
 | 
			
		||||
  sessionToken: string;
 | 
			
		||||
  sessionCookie: string;
 | 
			
		||||
@ -292,27 +303,20 @@ export async function createSessionWithCookie(userInfo: UserInfo): Promise<{
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * CONSOLIDATED: Replace repeated auth patterns in .astro pages
 | 
			
		||||
 * Usage: const authResult = await withAuth(Astro);
 | 
			
		||||
 *        if (authResult instanceof Response) return authResult;
 | 
			
		||||
 */
 | 
			
		||||
export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Response> {
 | 
			
		||||
  const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
 | 
			
		||||
  console.log('[DEBUG PAGE] Auth required:', authRequired);
 | 
			
		||||
export async function withAuth(Astro: AstroGlobal, context: AuthContextType = 'general'): Promise<AuthContext | Response> {
 | 
			
		||||
  const authRequired = getAuthRequirement(context);
 | 
			
		||||
  console.log(`[DEBUG PAGE] Auth required for ${context}:`, authRequired);
 | 
			
		||||
  console.log('[DEBUG PAGE] Request URL:', Astro.url.toString());
 | 
			
		||||
  
 | 
			
		||||
  // If auth not required, return mock context
 | 
			
		||||
  if (!authRequired) {
 | 
			
		||||
    return {
 | 
			
		||||
      authenticated: true,
 | 
			
		||||
      session: null,
 | 
			
		||||
      userEmail: 'anonymous@example.com',
 | 
			
		||||
      userEmail: 'anon@anon.anon',
 | 
			
		||||
      userId: 'anonymous'
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Check session
 | 
			
		||||
  const sessionToken = getSessionFromRequest(Astro.request);
 | 
			
		||||
  console.log('[DEBUG PAGE] Session token found:', !!sessionToken);
 | 
			
		||||
  
 | 
			
		||||
@ -337,7 +341,7 @@ export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Respon
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  console.log('[DEBUG PAGE] Page authentication successful for user:', session.userId);
 | 
			
		||||
  console.log(`[DEBUG PAGE] Page authentication successful for ${context}:`, session.userId);
 | 
			
		||||
  return {
 | 
			
		||||
    authenticated: true,
 | 
			
		||||
    session,
 | 
			
		||||
@ -346,17 +350,13 @@ export async function withAuth(Astro: AstroGlobal): Promise<AuthContext | Respon
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * CONSOLIDATED: Replace repeated auth patterns in API endpoints
 | 
			
		||||
 * Enhanced version with better return structure
 | 
			
		||||
 */
 | 
			
		||||
export async function withAPIAuth(request: Request): Promise<{ 
 | 
			
		||||
export async function withAPIAuth(request: Request, context: AuthContextType = 'general'): Promise<{ 
 | 
			
		||||
  authenticated: boolean; 
 | 
			
		||||
  userId: string; 
 | 
			
		||||
  session?: SessionData;
 | 
			
		||||
  authRequired: boolean; 
 | 
			
		||||
}> {
 | 
			
		||||
  const authRequired = process.env.AUTHENTICATION_NECESSARY !== 'false';
 | 
			
		||||
  const authRequired = getAuthRequirement(context);
 | 
			
		||||
  
 | 
			
		||||
  if (!authRequired) {
 | 
			
		||||
    return {
 | 
			
		||||
@ -367,10 +367,10 @@ export async function withAPIAuth(request: Request): Promise<{
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const sessionToken = getSessionFromRequest(request);
 | 
			
		||||
  console.log('[DEBUG] Session token found:', !!sessionToken);
 | 
			
		||||
  console.log(`[DEBUG API] Session token found for ${context}:`, !!sessionToken);
 | 
			
		||||
  
 | 
			
		||||
  if (!sessionToken) {
 | 
			
		||||
    console.log('[DEBUG] No session token found');
 | 
			
		||||
    console.log(`[DEBUG API] No session token found for ${context}`);
 | 
			
		||||
    return { 
 | 
			
		||||
      authenticated: false, 
 | 
			
		||||
      userId: '', 
 | 
			
		||||
@ -379,10 +379,10 @@ export async function withAPIAuth(request: Request): Promise<{
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const session = await verifySession(sessionToken);
 | 
			
		||||
  console.log('[DEBUG] Session verification result:', !!session);
 | 
			
		||||
  console.log(`[DEBUG API] Session verification result for ${context}:`, !!session);
 | 
			
		||||
  
 | 
			
		||||
  if (!session) {
 | 
			
		||||
    console.log('[DEBUG] Session verification failed');
 | 
			
		||||
    console.log(`[DEBUG API] Session verification failed for ${context}`);
 | 
			
		||||
    return { 
 | 
			
		||||
      authenticated: false, 
 | 
			
		||||
      userId: '', 
 | 
			
		||||
@ -390,7 +390,7 @@ export async function withAPIAuth(request: Request): Promise<{
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  console.log('[DEBUG] Authentication successful for user:', session.userId);
 | 
			
		||||
  console.log(`[DEBUG API] Authentication successful for ${context}:`, session.userId);
 | 
			
		||||
  return {
 | 
			
		||||
    authenticated: true,
 | 
			
		||||
    userId: session.userId,
 | 
			
		||||
@ -398,3 +398,7 @@ export async function withAPIAuth(request: Request): Promise<{
 | 
			
		||||
    authRequired: true  
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getAuthRequirementForContext(context: AuthContextType): boolean {
 | 
			
		||||
  return getAuthRequirement(context);
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user