project init
This commit is contained in:
83
apps/api/src/auth/guards/jwt-auth.guard.ts
Normal file
83
apps/api/src/auth/guards/jwt-auth.guard.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { SupabaseService } from '../supabase.service';
|
||||
import { UsersService } from '../../users/users.service';
|
||||
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
||||
import { JwtPayload } from '../auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private jwtService: JwtService,
|
||||
private authService: AuthService,
|
||||
private supabaseService: SupabaseService,
|
||||
private usersService: UsersService
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new UnauthorizedException('Missing or invalid authorization header');
|
||||
}
|
||||
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
|
||||
try {
|
||||
// 1) Try our own JWT (from POST /auth/exchange)
|
||||
try {
|
||||
const payload = this.jwtService.verify<JwtPayload>(token);
|
||||
const user = await this.authService.validateJwtPayload(payload);
|
||||
if (user) {
|
||||
request.user = user;
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Not our JWT or expired — try Supabase below
|
||||
}
|
||||
|
||||
// 2) Fallback: Supabase access_token (for backward compatibility)
|
||||
const supabaseUser = await this.supabaseService.verifyToken(token);
|
||||
if (!supabaseUser) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
|
||||
let user = await this.usersService.findBySupabaseId(supabaseUser.id);
|
||||
if (!user) {
|
||||
user = await this.usersService.create({
|
||||
supabaseId: supabaseUser.id,
|
||||
email: supabaseUser.email!,
|
||||
name:
|
||||
supabaseUser.user_metadata?.full_name ||
|
||||
supabaseUser.user_metadata?.name ||
|
||||
null,
|
||||
avatarUrl: supabaseUser.user_metadata?.avatar_url || null,
|
||||
});
|
||||
}
|
||||
|
||||
request.user = user;
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedException) throw error;
|
||||
throw new UnauthorizedException('Token validation failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
72
apps/api/src/auth/guards/supabase-auth.guard.ts
Normal file
72
apps/api/src/auth/guards/supabase-auth.guard.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { SupabaseService } from '../supabase.service';
|
||||
import { UsersService } from '../../users/users.service';
|
||||
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class SupabaseAuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private supabaseService: SupabaseService,
|
||||
private usersService: UsersService
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
// Check if route is public
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new UnauthorizedException('Missing or invalid authorization header');
|
||||
}
|
||||
|
||||
const token = authHeader.replace('Bearer ', '');
|
||||
|
||||
try {
|
||||
// Validate token with Supabase
|
||||
const supabaseUser = await this.supabaseService.verifyToken(token);
|
||||
|
||||
if (!supabaseUser) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
|
||||
// Find or create user in our database
|
||||
let user = await this.usersService.findBySupabaseId(supabaseUser.id);
|
||||
|
||||
if (!user) {
|
||||
// Auto-create user on first API call
|
||||
user = await this.usersService.create({
|
||||
supabaseId: supabaseUser.id,
|
||||
email: supabaseUser.email!,
|
||||
name:
|
||||
supabaseUser.user_metadata?.full_name ||
|
||||
supabaseUser.user_metadata?.name ||
|
||||
null,
|
||||
avatarUrl: supabaseUser.user_metadata?.avatar_url || null,
|
||||
});
|
||||
}
|
||||
|
||||
// Attach user to request
|
||||
request.user = user;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Token validation failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user