Skip to content

Commit 4251b4c

Browse files
STNDP-144 Handle todo comments and miscellaneous bugs - cleanup (#91)
* STNDP-144 Handle todo comments and miscellaneous bugs - cleanup * STNDP-144 Handle todo comments and miscellaneous bugs - cleanup * STNDP-144 Handle todo comments and miscellaneous bugs - cleanup * STNDP-144 Handle todo comments and miscellaneous bugs - cleanup * STNDP-144 Handle todo comments and miscellaneous bugs - cleanup * STNDP-144 Handle todo comments and miscellaneous bugs - cleanup * STNDP-144 Handle todo comments and miscellaneous bugs - cleanup
1 parent 3a9b442 commit 4251b4c

37 files changed

Lines changed: 1499 additions & 1095 deletions

apps/api/src/app.module.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,23 @@ import { Module } from '@nestjs/common';
22
import { AppController } from './app.controller';
33
import { AppService } from './app.service';
44
import { AuthModule } from './auth/auth.module';
5+
import { OtpModule } from './auth/otp/otp.module';
6+
import { UsersModule } from './auth/users/users.module';
7+
import { TokenModule } from './auth/token/token.module';
8+
import { SessionsModule } from './auth/sessions/sessions.module';
59
import { BoardsModule } from './boards/boards.module';
610
import { DbModule } from './db/db.module';
711

812
@Module({
9-
imports: [AuthModule, BoardsModule, DbModule],
13+
imports: [
14+
AuthModule,
15+
OtpModule,
16+
UsersModule,
17+
TokenModule,
18+
SessionsModule,
19+
BoardsModule,
20+
DbModule,
21+
],
1022
controllers: [AppController],
1123
providers: [AppService],
1224
})

apps/api/src/auth/auth.module.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { Module } from '@nestjs/common';
2-
import { OtpModule } from './otp/otp.module';
3-
import { UsersModule } from './users/users.module';
4-
import { TokenModule } from './token/token.module';
5-
import { SessionsModule } from './sessions/sessions.module';
2+
import { AuthService } from './auth.service';
3+
import { AuthGuard } from './guards/auth.guard';
4+
import { PermissiveAuthGuard } from './guards/permissive-auth.guard';
65

76
@Module({
87
controllers: [],
9-
imports: [OtpModule, UsersModule, TokenModule, SessionsModule],
8+
imports: [],
9+
providers: [AuthService, AuthGuard, PermissiveAuthGuard],
10+
exports: [AuthService, AuthGuard, PermissiveAuthGuard],
1011
})
1112
export class AuthModule {}

apps/api/src/auth/auth.service.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Injectable } from '@nestjs/common';
2+
import * as jose from 'jose';
3+
4+
@Injectable()
5+
export class AuthService {
6+
private readonly jwks = jose.createRemoteJWKSet(
7+
new URL(process.env.AUTH_SERVICE_JWKS_URL!),
8+
);
9+
10+
/**
11+
* Verifies a JWT token and returns the user ID.
12+
* Throws an error if the token is invalid.
13+
*/
14+
async verifyToken(token: string): Promise<string> {
15+
const { payload } = await jose.jwtVerify<{
16+
sub: string;
17+
iss: string;
18+
iat: number;
19+
aud: string;
20+
exp: number;
21+
}>(token, this.jwks);
22+
23+
return payload.sub;
24+
}
25+
26+
/**
27+
* Optionally verifies a JWT token and returns the user ID.
28+
* Returns null if the token is invalid or missing.
29+
*/
30+
async verifyTokenOptional(token?: string): Promise<string | null> {
31+
if (!token) {
32+
return null;
33+
}
34+
35+
try {
36+
return await this.verifyToken(token);
37+
} catch (error) {
38+
// Silently fail for optional verification
39+
return null;
40+
}
41+
}
42+
43+
/**
44+
* Extracts Bearer token from Authorization header.
45+
*/
46+
extractBearerToken(authHeader?: string): string | undefined {
47+
const [type, token] = authHeader?.split(' ') ?? [];
48+
return type === 'Bearer' ? token : undefined;
49+
}
50+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { UseGuards } from '@nestjs/common';
2+
import { PermissiveAuthGuard } from '../guards/permissive-auth.guard';
3+
4+
/**
5+
* Decorator that applies PermissiveAuthGuard.
6+
* Allows both authenticated and anonymous access, but enhances
7+
* the request with user data when authentication is available.
8+
*/
9+
export const PermissiveAuth = () => UseGuards(PermissiveAuthGuard);

apps/api/src/auth/guards/auth.guard.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,36 @@ import {
44
Injectable,
55
UnauthorizedException,
66
} from '@nestjs/common';
7-
import * as jose from 'jose';
87
import { Request } from 'express';
8+
import { AuthService } from '../auth.service';
99

1010
export interface AuthenticatedRequest extends Request {
1111
userId: string;
1212
}
1313

1414
@Injectable()
1515
export class AuthGuard implements CanActivate {
16-
constructor() {}
16+
constructor(private readonly authService: AuthService) {}
1717

1818
async canActivate(context: ExecutionContext): Promise<boolean> {
1919
const request = context.switchToHttp().getRequest<Request>();
2020

21-
const token = this.extractTokenFromHeader(request);
21+
const token = this.authService.extractBearerToken(
22+
request.headers.authorization,
23+
);
2224

2325
if (!token) {
2426
throw new UnauthorizedException();
2527
}
2628

27-
const jwks = jose.createRemoteJWKSet(
28-
new URL(process.env.AUTH_SERVICE_JWKS_URL!),
29-
);
30-
3129
try {
32-
const { payload } = await jose.jwtVerify<{
33-
sub: string;
34-
iss: string;
35-
iat: number;
36-
aud: string;
37-
exp: number;
38-
}>(token, jwks);
39-
(request as AuthenticatedRequest).userId = payload.sub;
30+
const userId = await this.authService.verifyToken(token);
31+
(request as AuthenticatedRequest).userId = userId;
4032
} catch (error) {
4133
console.error(error);
4234
throw new UnauthorizedException();
4335
}
4436

4537
return true;
4638
}
47-
48-
private extractTokenFromHeader(request: Request): string | undefined {
49-
const authHeader = request.headers.authorization;
50-
const [type, token] = authHeader?.split(' ') ?? [];
51-
return type === 'Bearer' ? token : undefined;
52-
}
5339
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
2+
import { Request } from 'express';
3+
import { AuthService } from '../auth.service';
4+
import { AuthenticatedRequest } from './auth.guard';
5+
6+
/**
7+
* PermissiveAuthGuard allows both authenticated and anonymous access.
8+
* If a valid Authorization header is present, it will populate req.userId.
9+
* If not, the request continues without authentication.
10+
*
11+
* This is useful for endpoints that provide enhanced functionality for
12+
* authenticated users but should remain publicly accessible.
13+
*/
14+
@Injectable()
15+
export class PermissiveAuthGuard implements CanActivate {
16+
constructor(private readonly authService: AuthService) {}
17+
18+
async canActivate(context: ExecutionContext): Promise<boolean> {
19+
const request = context.switchToHttp().getRequest<Request>();
20+
21+
const token = this.authService.extractBearerToken(
22+
request.headers.authorization,
23+
);
24+
25+
const userId = await this.authService.verifyTokenOptional(token);
26+
27+
if (userId) {
28+
(request as AuthenticatedRequest).userId = userId;
29+
}
30+
31+
// Always allow the request to proceed
32+
return true;
33+
}
34+
}

apps/api/src/auth/sessions/sessions.controller.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { Controller, Param, Post, UnauthorizedException } from '@nestjs/common';
1+
import {
2+
Controller,
3+
Param,
4+
Post,
5+
UnauthorizedException,
6+
ServiceUnavailableException,
7+
} from '@nestjs/common';
28
import { SessionsService } from './sessions.service';
39

410
@Controller('auth/sessions')
@@ -8,9 +14,18 @@ export class SessionsController {
814
@Post(':sessionId/refresh')
915
async refresh(@Param('sessionId') sessionId: string) {
1016
const response = await this.sessionsService.refresh(sessionId);
17+
1118
if ('code' in response) {
19+
// Check if it's a network error vs auth failure
20+
if (response.code === 'NETWORK_ERROR') {
21+
// Return 503 Service Unavailable for network issues
22+
throw new ServiceUnavailableException(response.error);
23+
}
24+
25+
// Return 401 Unauthorized for other auth failures
1226
throw new UnauthorizedException(response.error);
1327
}
28+
1429
return response;
1530
}
1631
}

apps/api/src/auth/sessions/sessions.service.ts

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,61 @@ import { Injectable } from '@nestjs/common';
22

33
@Injectable()
44
export class SessionsService {
5+
private isNetworkError(error: any): boolean {
6+
const networkErrorCodes = [
7+
'ENOTFOUND', // DNS lookup failed
8+
'ECONNREFUSED', // Connection refused
9+
'ETIMEDOUT', // Connection timeout
10+
'ECONNRESET', // Connection reset by peer
11+
'EHOSTUNREACH', // Host unreachable
12+
'ENETUNREACH', // Network unreachable
13+
'ECONNABORTED', // Connection aborted
14+
'EPIPE', // Broken pipe
15+
'EAI_AGAIN', // DNS lookup timeout
16+
];
17+
18+
const errorCode = error.code || error.cause?.code;
19+
return (
20+
networkErrorCodes.includes(errorCode) ||
21+
error.message?.includes('fetch failed') ||
22+
(error.name === 'TypeError' && error.message?.includes('Failed to fetch'))
23+
);
24+
}
25+
526
async refresh(
627
sessionId: string,
728
): Promise<{ access_token: string } | { code: string; error: string }> {
8-
return await fetch(
9-
process.env.AUTH_SERVICE_API_URL + '/auth/sessions/current/refresh',
10-
{
11-
method: 'POST',
12-
headers: {
13-
'X-Stack-Access-Type': 'server',
14-
'X-Stack-Project-Id': process.env.AUTH_SERVICE_PROJECT_ID!,
15-
'X-Stack-Secret-Server-Key':
16-
process.env.AUTH_SERVICE_SECRET_SERVER_KEY!,
17-
'X-Stack-Refresh-Token': sessionId,
29+
try {
30+
const response = await fetch(
31+
process.env.AUTH_SERVICE_API_URL + '/auth/sessions/current/refresh',
32+
{
33+
method: 'POST',
34+
headers: {
35+
'X-Stack-Access-Type': 'server',
36+
'X-Stack-Project-Id': process.env.AUTH_SERVICE_PROJECT_ID!,
37+
'X-Stack-Secret-Server-Key':
38+
process.env.AUTH_SERVICE_SECRET_SERVER_KEY!,
39+
'X-Stack-Refresh-Token': sessionId,
40+
},
1841
},
19-
},
20-
).then(
21-
(response) =>
22-
response.json() as Promise<
23-
| { access_token: string }
24-
| {
25-
code: string;
26-
error: string;
27-
}
28-
>,
29-
);
42+
);
43+
44+
return await response.json();
45+
} catch (error: any) {
46+
if (this.isNetworkError(error)) {
47+
// Return consistent error format for network issues
48+
return {
49+
code: 'NETWORK_ERROR',
50+
error:
51+
'Network connectivity issue - unable to reach authentication service',
52+
};
53+
}
54+
55+
// For other errors, treat as auth failure
56+
return {
57+
code: 'UNKNOWN_ERROR',
58+
error: error.message || 'Failed to refresh token',
59+
};
60+
}
3061
}
3162
}

apps/api/src/auth/token/token.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Module } from '@nestjs/common';
22
import { TokenController } from './token.controller';
3+
import { AuthModule } from '../auth.module';
34

45
@Module({
6+
imports: [AuthModule],
57
providers: [],
68
controllers: [TokenController],
79
})

apps/api/src/auth/users/users.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
22
import { UsersService } from './users.service';
33
import { UsersController } from './users.controller';
44
import { DbModule } from 'src/db/db.module';
5+
import { AuthModule } from '../auth.module';
56

67
@Module({
78
controllers: [UsersController],
89
providers: [UsersService],
9-
imports: [DbModule],
10+
imports: [DbModule, AuthModule],
1011
})
1112
export class UsersModule {}

0 commit comments

Comments
 (0)