Skip to content

Commit 865ee0a

Browse files
authored
Merge pull request #142 from import-ai/feature/cookie_auth
Feature/cookie auth
2 parents d2afbaf + ca5f9f3 commit 865ee0a

File tree

13 files changed

+513
-63
lines changed

13 files changed

+513
-63
lines changed

src/attachments/attachments.controller.ts

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ import { FilesInterceptor } from '@nestjs/platform-express';
1616
import { AttachmentsService } from 'omniboxd/attachments/attachments.service';
1717
import { Request, Response } from 'express';
1818
import { UserId } from 'omniboxd/decorators/user-id.decorator';
19-
import { Public } from 'omniboxd/auth/decorators/public.auth.decorator';
20-
import { Cookies } from 'omniboxd/decorators/cookie.decorators';
2119
import { AuthService } from 'omniboxd/auth/auth.service';
20+
import { CookieAuth } from 'omniboxd/auth';
2221

2322
@Controller('api/v1/attachments')
2423
export class AttachmentsController {
@@ -62,65 +61,47 @@ export class AttachmentsController {
6261
);
6362
}
6463

65-
@Public()
64+
setRedirect(req: Request, res: Response) {
65+
res
66+
.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
67+
.status(HttpStatus.FOUND)
68+
.redirect(`/user/login?redirect=${encodeURIComponent(req.url)}`);
69+
}
70+
71+
@CookieAuth({ onAuthFail: 'continue' })
6672
@Get('images/:attachmentId')
6773
async displayImage(
74+
@UserId({ optional: true }) userId: string | undefined,
6875
@Req() req: Request,
6976
@Res() res: Response,
70-
@Cookies('token') token: string,
7177
@Param('attachmentId') attachmentId: string,
7278
) {
73-
let userId = '';
74-
75-
if (token) {
76-
const payload = this.authService.jwtVerify(token);
77-
if (payload && payload.sub) {
78-
userId = payload.sub;
79-
}
80-
}
81-
82-
this.logger.debug({ userId, token, cookies: req.cookies });
8379
if (userId) {
84-
return await this.attachmentsService.displayImage(
80+
return await this.attachmentsService.displayMedia(
8581
attachmentId,
8682
userId,
8783
res,
8884
);
8985
}
90-
res
91-
.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
92-
.status(HttpStatus.FOUND)
93-
.redirect(`/user/login?redirect=${encodeURIComponent(req.url)}`);
86+
this.setRedirect(req, res);
9487
}
9588

96-
@Public()
89+
@CookieAuth({ onAuthFail: 'continue' })
9790
@Get('media/:attachmentId')
9891
async displayMedia(
92+
@UserId({ optional: true }) userId: string | undefined,
9993
@Req() req: Request,
10094
@Res() res: Response,
101-
@Cookies('token') token: string,
10295
@Param('attachmentId') attachmentId: string,
10396
) {
104-
let userId = '';
105-
106-
if (token) {
107-
const payload = this.authService.jwtVerify(token);
108-
if (payload && payload.sub) {
109-
userId = payload.sub;
110-
}
111-
}
112-
11397
if (userId) {
11498
return await this.attachmentsService.displayMedia(
11599
attachmentId,
116100
userId,
117101
res,
118102
);
119103
}
120-
res
121-
.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
122-
.status(HttpStatus.FOUND)
123-
.redirect(`/user/login?redirect=${encodeURIComponent(req.url)}`);
104+
this.setRedirect(req, res);
124105
}
125106

126107
@Delete(':attachmentId')

src/attachments/attachments.e2e-spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ describe('AttachmentsController (e2e)', () => {
339339
describe('GET /api/v1/attachments/media/:attachmentId (Public)', () => {
340340
let mediaAttachmentId: string;
341341
const mediaContent = Buffer.from('fake-media-content');
342-
const mediaFilename = 'test-media.mp4';
342+
const mediaFilename = 'test-media.mp3';
343343

344344
beforeAll(async () => {
345345
// Upload a media file to test display

src/attachments/attachments.service.ts

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,15 @@ export class AttachmentsService {
162162
return { id: attachmentId, success: true };
163163
}
164164

165+
isMedia(mimetype: string): boolean {
166+
for (const type of ['image/', 'audio/']) {
167+
if (mimetype.startsWith(type)) {
168+
return true;
169+
}
170+
}
171+
return false;
172+
}
173+
165174
async displayMedia(
166175
attachmentId: string,
167176
userId: string,
@@ -179,33 +188,11 @@ export class AttachmentsService {
179188
ResourcePermission.CAN_VIEW,
180189
);
181190
await this.checkAttachment(namespaceId, resourceId, attachmentId);
191+
if (!this.isMedia(objectResponse.mimetype)) {
192+
throw new BadRequestException(attachmentId);
193+
}
182194
return objectStreamResponse(objectResponse, httpResponse, {
183195
forceDownload: false,
184196
});
185197
}
186-
187-
async displayImage(
188-
attachmentId: string,
189-
userId: string,
190-
httpResponse: Response,
191-
) {
192-
const objectResponse = await this.minioService.get(
193-
this.minioPath(attachmentId),
194-
);
195-
const { namespaceId, resourceId } = objectResponse.metadata;
196-
197-
await this.checkPermission(
198-
namespaceId,
199-
resourceId,
200-
userId,
201-
ResourcePermission.CAN_VIEW,
202-
);
203-
await this.checkAttachment(namespaceId, resourceId, attachmentId);
204-
if (objectResponse.mimetype.startsWith('image/')) {
205-
return objectStreamResponse(objectResponse, httpResponse, {
206-
forceDownload: false,
207-
});
208-
}
209-
throw new BadRequestException(attachmentId);
210-
}
211198
}

src/auth/auth.module.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
22
import { JwtModule } from '@nestjs/jwt';
33
import { APP_GUARD } from '@nestjs/core';
44
import { PassportModule } from '@nestjs/passport';
5+
56
import { MailModule } from 'omniboxd/mail/mail.module';
67
import { UserModule } from 'omniboxd/user/user.module';
78
import { AuthService } from 'omniboxd/auth/auth.service';
@@ -18,6 +19,7 @@ import { WechatService } from 'omniboxd/auth/wechat.service';
1819
import { WechatController } from 'omniboxd/auth/wechat.controller';
1920
import { APIKeyModule } from 'omniboxd/api-key/api-key.module';
2021
import { APIKeyAuthGuard } from 'omniboxd/auth/api-key/api-key-auth.guard';
22+
import { CookieAuthGuard } from 'omniboxd/auth/cookie/cookie-auth.guard';
2123

2224
@Module({
2325
exports: [AuthService, WechatService],
@@ -35,6 +37,10 @@ import { APIKeyAuthGuard } from 'omniboxd/auth/api-key/api-key-auth.guard';
3537
provide: APP_GUARD,
3638
useClass: APIKeyAuthGuard,
3739
},
40+
{
41+
provide: APP_GUARD,
42+
useClass: CookieAuthGuard,
43+
},
3844
],
3945
imports: [
4046
UserModule,
@@ -44,6 +50,7 @@ import { APIKeyAuthGuard } from 'omniboxd/auth/api-key/api-key-auth.guard';
4450
GroupsModule,
4551
PermissionsModule,
4652
APIKeyModule,
53+
4754
JwtModule.registerAsync({
4855
imports: [ConfigModule],
4956
inject: [ConfigService],

src/auth/cookie/README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Cookie Authentication
2+
3+
Cookie-based authentication for protecting endpoints with JWT token cookies.
4+
5+
## Usage
6+
7+
### Basic Usage
8+
9+
```typescript
10+
import { Controller, Get } from '@nestjs/common';
11+
import { CookieAuth } from 'omniboxd/auth/decorators';
12+
import { UserId } from 'omniboxd/decorators/user-id.decorator';
13+
14+
@Controller('api/v1/protected')
15+
export class ProtectedController {
16+
@Get('data')
17+
@CookieAuth()
18+
getProtectedData(@UserId() userId: string) {
19+
return { message: 'Authenticated user', userId };
20+
}
21+
}
22+
```
23+
24+
### Authentication Options
25+
26+
```typescript
27+
// Strict authentication (default) - throws UnauthorizedException on failure
28+
@CookieAuth()
29+
30+
// Continue without authentication on failure
31+
@CookieAuth({ onAuthFail: 'continue' })
32+
```
33+
34+
## Authentication Flow
35+
36+
1. Client sends request with `token` cookie containing a JWT
37+
2. `CookieAuthGuard` validates the JWT token using `AuthService.jwtVerify()`
38+
3. User data is extracted from JWT payload and set on `request.user`
39+
4. Controller methods access user data via `@UserId()` decorator
40+
41+
## Requirements
42+
43+
- **Cookie Name**: `token`
44+
- **Token Format**: Valid JWT with `sub` field (user ID)
45+
- **Optional Fields**: `email`, `username`
46+
47+
## Error Handling
48+
49+
- **Missing/Invalid Token**: Throws `UnauthorizedException` (unless `onAuthFail: 'continue'`)
50+
- **Invalid Payload**: Throws `UnauthorizedException` if `sub` field missing
51+
52+
## Integration
53+
54+
Works alongside JWT and API key authentication:
55+
- `@CookieAuth()`: Cookie authentication
56+
- `@APIKeyAuth()`: API key authentication
57+
- `@Public()`: Skip authentication
58+
- Default: JWT authentication

0 commit comments

Comments
 (0)