Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "omniboxd",
"version": "0.1.4",
"version": "0.1.10",
"private": true,
"scripts": {
"build": "nest build",
Expand Down
107 changes: 78 additions & 29 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Response } from 'express';
import { AuthService } from 'omniboxd/auth/auth.service';
import { LocalAuthGuard } from 'omniboxd/auth/local-auth.guard';
import { Public } from 'omniboxd/auth/decorators/public.auth.decorator';
import { UserId } from 'omniboxd/decorators/user-id.decorator';
import { ConfigService } from '@nestjs/config';
import {
Res,
Expand All @@ -11,9 +12,15 @@ import {
UseGuards,
Controller,
HttpCode,
Query,
} from '@nestjs/common';
import { ResourcePermission } from 'omniboxd/permissions/resource-permission.enum';
import { NamespaceRole } from 'omniboxd/namespaces/entities/namespace-member.entity';
import {
SendEmailOtpDto,
VerifyEmailOtpDto,
SendEmailOtpResponseDto,
} from './dto/email-otp.dto';

@Controller('api/v1')
export class AuthController {
Expand Down Expand Up @@ -45,60 +52,77 @@ export class AuthController {
}

@Public()
@Post('sign-up')
async signUp(@Body('url') url: string, @Body('email') email: string) {
return await this.authService.signUp(url, email);
@Post('auth/send-otp')
@HttpCode(200)
async sendEmailOtp(
@Body() dto: SendEmailOtpDto,
@Body('url') url: string,
): Promise<SendEmailOtpResponseDto> {
return await this.authService.sendOTP(dto.email, url);
}

@Public()
@Post('auth/send-signup-otp')
@HttpCode(200)
async sendSignupOtp(
@Body() dto: SendEmailOtpDto,
@Body('url') url: string,
): Promise<SendEmailOtpResponseDto> {
return await this.authService.sendSignupOTP(dto.email, url);
}

@Public()
@Post('sign-up/confirm')
async signUpConfirm(
@Body('token') token: string,
@Body('username') username: string,
@Body('password') password: string,
@Post('auth/verify-otp')
@HttpCode(200)
async verifyEmailOtp(
@Body() dto: VerifyEmailOtpDto,
@Res() res: Response,
@Body('lang') lang?: string,
) {
const signUpData = await this.authService.signUpConfirm(token, {
username,
password,
const authData = await this.authService.verifyOTP(
dto.email,
dto.code,
lang,
});
);

const jwtExpireSeconds = parseInt(
this.configService.get('OBB_JWT_EXPIRE', '2678400'),
10,
);
res.cookie('token', signUpData.access_token, {
res.cookie('token', authData.access_token, {
httpOnly: true,
secure: true,
sameSite: 'none',
path: '/',
maxAge: jwtExpireSeconds * 1000,
});

return res.json(signUpData);
}

@Public()
@Post('password')
async password(@Body('url') url: string, @Body('email') email: string) {
return await this.authService.password(url, email);
return res.json(authData);
}

@Public()
@Post('password/confirm')
async resetPassword(
@Body('token') token: string,
@Body('password') password: string,
@Post('auth/verify-magic')
@HttpCode(200)
async verifyMagicLink(
@Query('token') token: string,
@Res() res: Response,
@Body('lang') lang?: string,
) {
const result = await this.authService.resetPassword(token, password);
res.clearCookie('token', {
const authData = await this.authService.verifyMagicLink(token, lang);

const jwtExpireSeconds = parseInt(
this.configService.get('OBB_JWT_EXPIRE', '2678400'),
10,
);
res.cookie('token', authData.access_token, {
httpOnly: true,
secure: true,
sameSite: 'none',
path: '/',
maxAge: jwtExpireSeconds * 1000,
});
return res.json(result);

return res.json(authData);
}

@Post('invite')
Expand Down Expand Up @@ -140,8 +164,33 @@ export class AuthController {
}

@Post('invite/confirm')
async inviteConfirm(@Body('token') token: string) {
return await this.authService.inviteConfirm(token);
async inviteConfirm(@UserId() userId: string, @Body('token') token: string) {
return await this.authService.inviteConfirm(token, userId);
}

@Public()
@Post('auth/accept-invite')
@HttpCode(200)
async acceptInvite(
@Query('token') token: string,
@Res() res: Response,
@Body('lang') lang?: string,
) {
const authData = await this.authService.acceptInvite(token, lang);

const jwtExpireSeconds = parseInt(
this.configService.get('OBB_JWT_EXPIRE', '2678400'),
10,
);
res.cookie('token', authData.access_token, {
httpOnly: true,
secure: true,
sameSite: 'none',
path: '/',
maxAge: jwtExpireSeconds * 1000,
});

return res.json(authData);
}

@Post('logout')
Expand Down
63 changes: 0 additions & 63 deletions src/auth/auth.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,18 +254,6 @@ describe('AuthModule (e2e)', () => {
});
});

it('should access sign-up endpoint without authentication', async () => {
// Note: This will fail due to email service in test environment, but endpoint should be accessible
await client
.request()
.post('/api/v1/sign-up')
.send({
url: 'http://localhost:3000/signup',
email: 'test-signup@example.com',
})
.expect(201);
});

it('should access WeChat QR code endpoint without authentication', async () => {
await client
.request()
Expand Down Expand Up @@ -484,55 +472,4 @@ describe('AuthModule (e2e)', () => {
.expect(HttpStatus.UNAUTHORIZED);
});
});

describe('Sign-up Flow', () => {
describe('POST /api/v1/sign-up/confirm', () => {
it('should fail with invalid token', async () => {
await client
.request()
.post('/api/v1/sign-up/confirm')
.send({
token: 'invalid-token',
username: 'testuser',
password: 'testpassword',
})
.expect(HttpStatus.UNAUTHORIZED);
});

it('should fail with missing parameters', async () => {
await client
.request()
.post('/api/v1/sign-up/confirm')
.send({
token: 'some-token',
// Missing username and password
})
.expect(HttpStatus.UNAUTHORIZED); // Invalid token gets processed first
});
});

describe('POST /api/v1/password', () => {
it('should initiate password reset for existing user', async () => {
await client
.request()
.post('/api/v1/password')
.send({
url: 'http://localhost:3000/reset-password',
email: client.user.email,
})
.expect(201);
});

it('should fail for non-existent user', async () => {
await client
.request()
.post('/api/v1/password')
.send({
url: 'http://localhost:3000/reset-password',
email: 'nonexistent@example.com',
})
.expect(HttpStatus.NOT_FOUND);
});
});
});
});
2 changes: 2 additions & 0 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { WechatController } from 'omniboxd/auth/wechat/wechat.controller';
import { GoogleService } from 'omniboxd/auth/google/google.service';
import { GoogleController } from 'omniboxd/auth/google/google.controller';
import { SocialService } from 'omniboxd/auth/social.service';
import { OtpService } from 'omniboxd/auth/otp.service';
import { APIKeyModule } from 'omniboxd/api-key/api-key.module';
import { APIKeyAuthGuard } from 'omniboxd/auth/api-key/api-key-auth.guard';
import { CookieAuthGuard } from 'omniboxd/auth/cookie/cookie-auth.guard';
Expand All @@ -36,6 +37,7 @@ import { CacheService } from 'omniboxd/common/cache.service';
providers: [
AuthService,
SocialService,
OtpService,
WechatService,
GoogleService,
JwtStrategy,
Expand Down
Loading