feat: implement authentication and database modules with relations and group management
Added new authentication strategies (JWT and GitHub OAuth), guards, and controllers. Implemented database module, schema with relations, and group management features, including CRD operations and person-to-group associations. Integrated validation and CORS configuration.
This commit is contained in:
37
backend/src/modules/auth/auth.module.ts
Normal file
37
backend/src/modules/auth/auth.module.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { AuthController } from './controllers/auth.controller';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { GithubStrategy } from './strategies/github.strategy';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { JwtRefreshStrategy } from './strategies/jwt-refresh.strategy';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule,
|
||||
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get<string>('JWT_SECRET'),
|
||||
signOptions: {
|
||||
expiresIn: configService.get<string>('JWT_EXPIRATION') || '15m',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
UsersModule,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [
|
||||
AuthService,
|
||||
GithubStrategy,
|
||||
JwtStrategy,
|
||||
JwtRefreshStrategy,
|
||||
],
|
||||
exports: [AuthService, JwtStrategy, JwtRefreshStrategy, PassportModule],
|
||||
})
|
||||
export class AuthModule {}
|
||||
78
backend/src/modules/auth/controllers/auth.controller.ts
Normal file
78
backend/src/modules/auth/controllers/auth.controller.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Req,
|
||||
Res,
|
||||
UnauthorizedException,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Request, Response } from 'express';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { RefreshTokenDto } from '../dto/refresh-token.dto';
|
||||
import { GithubAuthGuard } from '../guards/github-auth.guard';
|
||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||
import { JwtRefreshGuard } from '../guards/jwt-refresh.guard';
|
||||
import { GetUser } from '../decorators/get-user.decorator';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initiate GitHub OAuth flow
|
||||
*/
|
||||
@Get('github')
|
||||
@UseGuards(GithubAuthGuard)
|
||||
githubAuth() {
|
||||
// This route is handled by the GitHub strategy
|
||||
// The actual implementation is in the GithubAuthGuard
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GitHub OAuth callback
|
||||
*/
|
||||
@Get('github/callback')
|
||||
@UseGuards(GithubAuthGuard)
|
||||
async githubAuthCallback(@Req() req: Request, @Res() res: Response) {
|
||||
// The user is already validated by the GitHub strategy
|
||||
// and attached to the request object
|
||||
const user = req.user as any;
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Authentication failed');
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
const tokens = await this.authService.generateTokens(user.id);
|
||||
|
||||
// Redirect to the frontend with tokens
|
||||
const frontendUrl = this.configService.get<string>('FRONTEND_URL') || 'http://localhost:3000';
|
||||
const redirectUrl = `${frontendUrl}/auth/callback?accessToken=${tokens.accessToken}&refreshToken=${tokens.refreshToken}`;
|
||||
|
||||
return res.redirect(redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh tokens
|
||||
*/
|
||||
@Post('refresh')
|
||||
@UseGuards(JwtRefreshGuard)
|
||||
async refreshTokens(@GetUser() user) {
|
||||
return this.authService.refreshTokens(user.id, user.refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user profile
|
||||
*/
|
||||
@Get('profile')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
getProfile(@GetUser() user) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
18
backend/src/modules/auth/decorators/get-user.decorator.ts
Normal file
18
backend/src/modules/auth/decorators/get-user.decorator.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* Decorator to extract user information from the request
|
||||
*
|
||||
* Usage:
|
||||
* - @GetUser() user: any - Get the entire user object
|
||||
* - @GetUser('id') userId: string - Get a specific property from the user object
|
||||
*/
|
||||
export const GetUser = createParamDecorator(
|
||||
(data: string | undefined, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
// Return the specific property if data is provided, otherwise return the entire user object
|
||||
return data ? user?.[data] : user;
|
||||
},
|
||||
);
|
||||
13
backend/src/modules/auth/dto/refresh-token.dto.ts
Normal file
13
backend/src/modules/auth/dto/refresh-token.dto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
/**
|
||||
* DTO for refresh token request
|
||||
*/
|
||||
export class RefreshTokenDto {
|
||||
/**
|
||||
* The refresh token
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
refreshToken: string;
|
||||
}
|
||||
8
backend/src/modules/auth/guards/github-auth.guard.ts
Normal file
8
backend/src/modules/auth/guards/github-auth.guard.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
/**
|
||||
* Guard for GitHub OAuth authentication
|
||||
*/
|
||||
@Injectable()
|
||||
export class GithubAuthGuard extends AuthGuard('github') {}
|
||||
18
backend/src/modules/auth/guards/jwt-auth.guard.ts
Normal file
18
backend/src/modules/auth/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
/**
|
||||
* Guard for JWT authentication
|
||||
*/
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
/**
|
||||
* Handle unauthorized errors
|
||||
*/
|
||||
handleRequest(err: any, user: any, info: any) {
|
||||
if (err || !user) {
|
||||
throw err || new UnauthorizedException('Authentication required');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
18
backend/src/modules/auth/guards/jwt-refresh.guard.ts
Normal file
18
backend/src/modules/auth/guards/jwt-refresh.guard.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
/**
|
||||
* Guard for JWT refresh token authentication
|
||||
*/
|
||||
@Injectable()
|
||||
export class JwtRefreshGuard extends AuthGuard('jwt-refresh') {
|
||||
/**
|
||||
* Handle unauthorized errors
|
||||
*/
|
||||
handleRequest(err: any, user: any, info: any) {
|
||||
if (err || !user) {
|
||||
throw err || new UnauthorizedException('Valid refresh token required');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
24
backend/src/modules/auth/interfaces/jwt-payload.interface.ts
Normal file
24
backend/src/modules/auth/interfaces/jwt-payload.interface.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Interface for JWT payload
|
||||
*/
|
||||
export interface JwtPayload {
|
||||
/**
|
||||
* Subject (user ID)
|
||||
*/
|
||||
sub: string;
|
||||
|
||||
/**
|
||||
* Flag to indicate if this is a refresh token
|
||||
*/
|
||||
isRefreshToken?: boolean;
|
||||
|
||||
/**
|
||||
* Token issued at timestamp
|
||||
*/
|
||||
iat?: number;
|
||||
|
||||
/**
|
||||
* Token expiration timestamp
|
||||
*/
|
||||
exp?: number;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Interface for tokens response
|
||||
*/
|
||||
export interface TokensResponse {
|
||||
/**
|
||||
* JWT access token
|
||||
*/
|
||||
accessToken: string;
|
||||
|
||||
/**
|
||||
* JWT refresh token
|
||||
*/
|
||||
refreshToken: string;
|
||||
}
|
||||
96
backend/src/modules/auth/services/auth.service.ts
Normal file
96
backend/src/modules/auth/services/auth.service.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { UsersService } from '../../users/services/users.service';
|
||||
import { JwtPayload } from '../interfaces/jwt-payload.interface';
|
||||
import { TokensResponse } from '../interfaces/tokens-response.interface';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly usersService: UsersService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Validate a user by GitHub ID
|
||||
*/
|
||||
async validateGithubUser(
|
||||
githubId: string,
|
||||
email: string,
|
||||
name: string,
|
||||
avatarUrl: string,
|
||||
) {
|
||||
// Try to find the user by GitHub ID
|
||||
let user = await this.usersService.findByGithubId(githubId);
|
||||
|
||||
// If user doesn't exist, create a new one
|
||||
if (!user) {
|
||||
user = await this.usersService.create({
|
||||
githubId,
|
||||
name,
|
||||
avatar: avatarUrl,
|
||||
metadata: { email },
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JWT tokens (access and refresh)
|
||||
*/
|
||||
async generateTokens(userId: string): Promise<TokensResponse> {
|
||||
const payload: JwtPayload = { sub: userId };
|
||||
|
||||
const [accessToken, refreshToken] = await Promise.all([
|
||||
this.jwtService.signAsync(payload),
|
||||
this.jwtService.signAsync(
|
||||
{ ...payload, isRefreshToken: true },
|
||||
{
|
||||
expiresIn: this.configService.get<string>('JWT_REFRESH_EXPIRATION') || '7d',
|
||||
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh tokens using a valid refresh token
|
||||
*/
|
||||
async refreshTokens(userId: string, refreshToken: string): Promise<TokensResponse> {
|
||||
// Verify the refresh token
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(refreshToken, {
|
||||
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
|
||||
});
|
||||
|
||||
// Check if the token is a refresh token and belongs to the user
|
||||
if (!payload.isRefreshToken || payload.sub !== userId) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
|
||||
// Generate new tokens
|
||||
return this.generateTokens(userId);
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a user by JWT payload
|
||||
*/
|
||||
async validateJwtUser(payload: JwtPayload) {
|
||||
const user = await this.usersService.findById(payload.sub);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('User not found');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
50
backend/src/modules/auth/strategies/github.strategy.ts
Normal file
50
backend/src/modules/auth/strategies/github.strategy.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy } from 'passport-github2';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class GithubStrategy extends PassportStrategy(Strategy, 'github') {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly authService: AuthService,
|
||||
) {
|
||||
const clientID = configService.get<string>('GITHUB_CLIENT_ID') || 'dummy-client-id';
|
||||
const clientSecret = configService.get<string>('GITHUB_CLIENT_SECRET') || 'dummy-client-secret';
|
||||
const callbackURL = configService.get<string>('GITHUB_CALLBACK_URL') || 'http://localhost:3001/api/auth/github/callback';
|
||||
|
||||
super({
|
||||
clientID,
|
||||
clientSecret,
|
||||
callbackURL,
|
||||
scope: ['user:email'],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the GitHub profile and return the user
|
||||
*/
|
||||
async validate(accessToken: string, refreshToken: string, profile: any) {
|
||||
// Extract user information from GitHub profile
|
||||
const { id, displayName, emails, photos } = profile;
|
||||
|
||||
// Get primary email or first email
|
||||
const email = emails && emails.length > 0
|
||||
? (emails.find(e => e.primary)?.value || emails[0].value)
|
||||
: null;
|
||||
|
||||
// Get avatar URL
|
||||
const avatarUrl = photos && photos.length > 0 ? photos[0].value : null;
|
||||
|
||||
// Validate or create user
|
||||
const user = await this.authService.validateGithubUser(
|
||||
id,
|
||||
email,
|
||||
displayName || 'GitHub User',
|
||||
avatarUrl,
|
||||
);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
51
backend/src/modules/auth/strategies/jwt-refresh.strategy.ts
Normal file
51
backend/src/modules/auth/strategies/jwt-refresh.strategy.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { JwtPayload } from '../interfaces/jwt-payload.interface';
|
||||
|
||||
@Injectable()
|
||||
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt-refresh') {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly authService: AuthService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.get<string>('JWT_REFRESH_SECRET'),
|
||||
passReqToCallback: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the JWT refresh token payload and return the user
|
||||
*/
|
||||
async validate(req: any, payload: JwtPayload) {
|
||||
try {
|
||||
// Check if this is a refresh token
|
||||
if (!payload.isRefreshToken) {
|
||||
throw new UnauthorizedException('Invalid token type');
|
||||
}
|
||||
|
||||
// Extract the refresh token from the request
|
||||
const refreshToken = ExtractJwt.fromAuthHeaderAsBearerToken()(req);
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new UnauthorizedException('Refresh token not found');
|
||||
}
|
||||
|
||||
// Validate the user
|
||||
const user = await this.authService.validateJwtUser(payload);
|
||||
|
||||
// Attach the refresh token to the user object for later use
|
||||
return {
|
||||
...user,
|
||||
refreshToken,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
}
|
||||
}
|
||||
38
backend/src/modules/auth/strategies/jwt.strategy.ts
Normal file
38
backend/src/modules/auth/strategies/jwt.strategy.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { JwtPayload } from '../interfaces/jwt-payload.interface';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly authService: AuthService,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.get<string>('JWT_SECRET'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the JWT payload and return the user
|
||||
*/
|
||||
async validate(payload: JwtPayload) {
|
||||
try {
|
||||
// Check if this is a refresh token
|
||||
if (payload.isRefreshToken) {
|
||||
throw new UnauthorizedException('Invalid token type');
|
||||
}
|
||||
|
||||
// Validate the user
|
||||
const user = await this.authService.validateJwtUser(payload);
|
||||
return user;
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
}
|
||||
}
|
||||
94
backend/src/modules/groups/controllers/groups.controller.ts
Normal file
94
backend/src/modules/groups/controllers/groups.controller.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
Delete,
|
||||
Put,
|
||||
UseGuards,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { GroupsService } from '../services/groups.service';
|
||||
import { CreateGroupDto } from '../dto/create-group.dto';
|
||||
import { UpdateGroupDto } from '../dto/update-group.dto';
|
||||
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
|
||||
|
||||
@Controller('groups')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class GroupsController {
|
||||
constructor(private readonly groupsService: GroupsService) {}
|
||||
|
||||
/**
|
||||
* Create a new group
|
||||
*/
|
||||
@Post()
|
||||
create(@Body() createGroupDto: CreateGroupDto) {
|
||||
return this.groupsService.create(createGroupDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all groups or filter by project ID
|
||||
*/
|
||||
@Get()
|
||||
findAll(@Query('projectId') projectId?: string) {
|
||||
if (projectId) {
|
||||
return this.groupsService.findByProjectId(projectId);
|
||||
}
|
||||
return this.groupsService.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a group by ID
|
||||
*/
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.groupsService.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a group
|
||||
*/
|
||||
@Put(':id')
|
||||
update(@Param('id') id: string, @Body() updateGroupDto: UpdateGroupDto) {
|
||||
return this.groupsService.update(id, updateGroupDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a group
|
||||
*/
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.groupsService.remove(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a person to a group
|
||||
*/
|
||||
@Post(':id/persons/:personId')
|
||||
addPersonToGroup(
|
||||
@Param('id') groupId: string,
|
||||
@Param('personId') personId: string,
|
||||
) {
|
||||
return this.groupsService.addPersonToGroup(groupId, personId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a person from a group
|
||||
*/
|
||||
@Delete(':id/persons/:personId')
|
||||
removePersonFromGroup(
|
||||
@Param('id') groupId: string,
|
||||
@Param('personId') personId: string,
|
||||
) {
|
||||
return this.groupsService.removePersonFromGroup(groupId, personId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all persons in a group
|
||||
*/
|
||||
@Get(':id/persons')
|
||||
getPersonsInGroup(@Param('id') groupId: string) {
|
||||
return this.groupsService.getPersonsInGroup(groupId);
|
||||
}
|
||||
}
|
||||
27
backend/src/modules/groups/dto/create-group.dto.ts
Normal file
27
backend/src/modules/groups/dto/create-group.dto.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { IsNotEmpty, IsString, IsUUID, IsObject, IsOptional } from 'class-validator';
|
||||
|
||||
/**
|
||||
* DTO for creating a new group
|
||||
*/
|
||||
export class CreateGroupDto {
|
||||
/**
|
||||
* The name of the group
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The ID of the project this group belongs to
|
||||
*/
|
||||
@IsNotEmpty()
|
||||
@IsUUID()
|
||||
projectId: string;
|
||||
|
||||
/**
|
||||
* Optional metadata for the group
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
27
backend/src/modules/groups/dto/update-group.dto.ts
Normal file
27
backend/src/modules/groups/dto/update-group.dto.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { IsString, IsUUID, IsObject, IsOptional } from 'class-validator';
|
||||
|
||||
/**
|
||||
* DTO for updating an existing group
|
||||
*/
|
||||
export class UpdateGroupDto {
|
||||
/**
|
||||
* The name of the group
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* The ID of the project this group belongs to
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
projectId?: string;
|
||||
|
||||
/**
|
||||
* Metadata for the group
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
10
backend/src/modules/groups/groups.module.ts
Normal file
10
backend/src/modules/groups/groups.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { GroupsController } from './controllers/groups.controller';
|
||||
import { GroupsService } from './services/groups.service';
|
||||
|
||||
@Module({
|
||||
controllers: [GroupsController],
|
||||
providers: [GroupsService],
|
||||
exports: [GroupsService],
|
||||
})
|
||||
export class GroupsModule {}
|
||||
167
backend/src/modules/groups/services/groups.service.ts
Normal file
167
backend/src/modules/groups/services/groups.service.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Injectable, NotFoundException, Inject } from '@nestjs/common';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { DRIZZLE } from '../../../database/database.module';
|
||||
import * as schema from '../../../database/schema';
|
||||
import { CreateGroupDto } from '../dto/create-group.dto';
|
||||
import { UpdateGroupDto } from '../dto/update-group.dto';
|
||||
|
||||
@Injectable()
|
||||
export class GroupsService {
|
||||
constructor(@Inject(DRIZZLE) private readonly db: any) {}
|
||||
|
||||
/**
|
||||
* Create a new group
|
||||
*/
|
||||
async create(createGroupDto: CreateGroupDto) {
|
||||
const [group] = await this.db
|
||||
.insert(schema.groups)
|
||||
.values({
|
||||
...createGroupDto,
|
||||
})
|
||||
.returning();
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all groups
|
||||
*/
|
||||
async findAll() {
|
||||
return this.db.select().from(schema.groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find groups by project ID
|
||||
*/
|
||||
async findByProjectId(projectId: string) {
|
||||
return this.db
|
||||
.select()
|
||||
.from(schema.groups)
|
||||
.where(eq(schema.groups.projectId, projectId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a group by ID
|
||||
*/
|
||||
async findById(id: string) {
|
||||
const [group] = await this.db
|
||||
.select()
|
||||
.from(schema.groups)
|
||||
.where(eq(schema.groups.id, id));
|
||||
|
||||
if (!group) {
|
||||
throw new NotFoundException(`Group with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a group
|
||||
*/
|
||||
async update(id: string, updateGroupDto: UpdateGroupDto) {
|
||||
const [group] = await this.db
|
||||
.update(schema.groups)
|
||||
.set({
|
||||
...updateGroupDto,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.groups.id, id))
|
||||
.returning();
|
||||
|
||||
if (!group) {
|
||||
throw new NotFoundException(`Group with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a group
|
||||
*/
|
||||
async remove(id: string) {
|
||||
const [group] = await this.db
|
||||
.delete(schema.groups)
|
||||
.where(eq(schema.groups.id, id))
|
||||
.returning();
|
||||
|
||||
if (!group) {
|
||||
throw new NotFoundException(`Group with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a person to a group
|
||||
*/
|
||||
async addPersonToGroup(groupId: string, personId: string) {
|
||||
// Check if the group exists
|
||||
await this.findById(groupId);
|
||||
|
||||
// Check if the person exists
|
||||
const [person] = await this.db
|
||||
.select()
|
||||
.from(schema.persons)
|
||||
.where(eq(schema.persons.id, personId));
|
||||
|
||||
if (!person) {
|
||||
throw new NotFoundException(`Person with ID ${personId} not found`);
|
||||
}
|
||||
|
||||
// Check if the person is already in the group
|
||||
const [existingRelation] = await this.db
|
||||
.select()
|
||||
.from(schema.personToGroup)
|
||||
.where(eq(schema.personToGroup.personId, personId))
|
||||
.where(eq(schema.personToGroup.groupId, groupId));
|
||||
|
||||
if (existingRelation) {
|
||||
return existingRelation;
|
||||
}
|
||||
|
||||
// Add the person to the group
|
||||
const [relation] = await this.db
|
||||
.insert(schema.personToGroup)
|
||||
.values({
|
||||
personId,
|
||||
groupId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return relation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a person from a group
|
||||
*/
|
||||
async removePersonFromGroup(groupId: string, personId: string) {
|
||||
const [relation] = await this.db
|
||||
.delete(schema.personToGroup)
|
||||
.where(eq(schema.personToGroup.personId, personId))
|
||||
.where(eq(schema.personToGroup.groupId, groupId))
|
||||
.returning();
|
||||
|
||||
if (!relation) {
|
||||
throw new NotFoundException(`Person with ID ${personId} is not in group with ID ${groupId}`);
|
||||
}
|
||||
|
||||
return relation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all persons in a group
|
||||
*/
|
||||
async getPersonsInGroup(groupId: string) {
|
||||
// Check if the group exists
|
||||
await this.findById(groupId);
|
||||
|
||||
// Get all persons in the group
|
||||
return this.db
|
||||
.select({
|
||||
person: schema.persons,
|
||||
})
|
||||
.from(schema.personToGroup)
|
||||
.innerJoin(schema.persons, eq(schema.personToGroup.personId, schema.persons.id))
|
||||
.where(eq(schema.personToGroup.groupId, groupId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { PersonsService } from '../services/persons.service';
|
||||
import { CreatePersonDto } from '../dto/create-person.dto';
|
||||
import { UpdatePersonDto } from '../dto/update-person.dto';
|
||||
|
||||
@Controller('persons')
|
||||
export class PersonsController {
|
||||
constructor(private readonly personsService: PersonsService) {}
|
||||
|
||||
/**
|
||||
* Create a new person
|
||||
*/
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
create(@Body() createPersonDto: CreatePersonDto) {
|
||||
return this.personsService.create(createPersonDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all persons or filter by project ID
|
||||
*/
|
||||
@Get()
|
||||
findAll(@Query('projectId') projectId?: string) {
|
||||
if (projectId) {
|
||||
return this.personsService.findByProjectId(projectId);
|
||||
}
|
||||
return this.personsService.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a person by ID
|
||||
*/
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.personsService.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a person
|
||||
*/
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() updatePersonDto: UpdatePersonDto) {
|
||||
return this.personsService.update(id, updatePersonDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a person
|
||||
*/
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
remove(@Param('id') id: string) {
|
||||
return this.personsService.remove(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get persons by project ID and group ID
|
||||
*/
|
||||
@Get('project/:projectId/group/:groupId')
|
||||
findByProjectIdAndGroupId(
|
||||
@Param('projectId') projectId: string,
|
||||
@Param('groupId') groupId: string,
|
||||
) {
|
||||
return this.personsService.findByProjectIdAndGroupId(projectId, groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a person to a group
|
||||
*/
|
||||
@Post(':id/groups/:groupId')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
addToGroup(@Param('id') id: string, @Param('groupId') groupId: string) {
|
||||
return this.personsService.addToGroup(id, groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a person from a group
|
||||
*/
|
||||
@Delete(':id/groups/:groupId')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
removeFromGroup(@Param('id') id: string, @Param('groupId') groupId: string) {
|
||||
return this.personsService.removeFromGroup(id, groupId);
|
||||
}
|
||||
}
|
||||
83
backend/src/modules/persons/dto/create-person.dto.ts
Normal file
83
backend/src/modules/persons/dto/create-person.dto.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsObject,
|
||||
IsUUID,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsBoolean,
|
||||
Min,
|
||||
Max
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
/**
|
||||
* Enum for gender values
|
||||
*/
|
||||
export enum Gender {
|
||||
MALE = 'MALE',
|
||||
FEMALE = 'FEMALE',
|
||||
NON_BINARY = 'NON_BINARY',
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum for oral ease level values
|
||||
*/
|
||||
export enum OralEaseLevel {
|
||||
SHY = 'SHY',
|
||||
RESERVED = 'RESERVED',
|
||||
COMFORTABLE = 'COMFORTABLE',
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for creating a new person
|
||||
*/
|
||||
export class CreatePersonDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
firstName: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
lastName: string;
|
||||
|
||||
@IsEnum(Gender)
|
||||
@IsNotEmpty()
|
||||
gender: Gender;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(5)
|
||||
@Type(() => Number)
|
||||
technicalLevel: number;
|
||||
|
||||
@IsBoolean()
|
||||
@Type(() => Boolean)
|
||||
hasTechnicalTraining: boolean;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(5)
|
||||
@Type(() => Number)
|
||||
frenchSpeakingLevel: number;
|
||||
|
||||
@IsEnum(OralEaseLevel)
|
||||
@IsNotEmpty()
|
||||
oralEaseLevel: OralEaseLevel;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Min(18)
|
||||
@Max(100)
|
||||
@Type(() => Number)
|
||||
age?: number;
|
||||
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
projectId: string;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
attributes?: Record<string, any>;
|
||||
}
|
||||
68
backend/src/modules/persons/dto/update-person.dto.ts
Normal file
68
backend/src/modules/persons/dto/update-person.dto.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsObject,
|
||||
IsUUID,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsBoolean,
|
||||
Min,
|
||||
Max
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { Gender, OralEaseLevel } from './create-person.dto';
|
||||
|
||||
/**
|
||||
* DTO for updating a person
|
||||
*/
|
||||
export class UpdatePersonDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
firstName?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
lastName?: string;
|
||||
|
||||
@IsEnum(Gender)
|
||||
@IsOptional()
|
||||
gender?: Gender;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(5)
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
technicalLevel?: number;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Type(() => Boolean)
|
||||
hasTechnicalTraining?: boolean;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(5)
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
frenchSpeakingLevel?: number;
|
||||
|
||||
@IsEnum(OralEaseLevel)
|
||||
@IsOptional()
|
||||
oralEaseLevel?: OralEaseLevel;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Min(18)
|
||||
@Max(100)
|
||||
@Type(() => Number)
|
||||
age?: number;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
projectId?: string;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
attributes?: Record<string, any>;
|
||||
}
|
||||
145
backend/src/modules/persons/services/persons.service.ts
Normal file
145
backend/src/modules/persons/services/persons.service.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { Injectable, NotFoundException, Inject } from '@nestjs/common';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { DRIZZLE } from '../../../database/database.module';
|
||||
import * as schema from '../../../database/schema';
|
||||
import { CreatePersonDto } from '../dto/create-person.dto';
|
||||
import { UpdatePersonDto } from '../dto/update-person.dto';
|
||||
|
||||
@Injectable()
|
||||
export class PersonsService {
|
||||
constructor(@Inject(DRIZZLE) private readonly db: any) {}
|
||||
|
||||
/**
|
||||
* Create a new person
|
||||
*/
|
||||
async create(createPersonDto: CreatePersonDto) {
|
||||
const [person] = await this.db
|
||||
.insert(schema.persons)
|
||||
.values(createPersonDto)
|
||||
.returning();
|
||||
return person;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all persons
|
||||
*/
|
||||
async findAll() {
|
||||
return this.db.select().from(schema.persons);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find persons by project ID
|
||||
*/
|
||||
async findByProjectId(projectId: string) {
|
||||
return this.db
|
||||
.select()
|
||||
.from(schema.persons)
|
||||
.where(eq(schema.persons.projectId, projectId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a person by ID
|
||||
*/
|
||||
async findById(id: string) {
|
||||
const [person] = await this.db
|
||||
.select()
|
||||
.from(schema.persons)
|
||||
.where(eq(schema.persons.id, id));
|
||||
|
||||
if (!person) {
|
||||
throw new NotFoundException(`Person with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return person;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a person
|
||||
*/
|
||||
async update(id: string, updatePersonDto: UpdatePersonDto) {
|
||||
const [person] = await this.db
|
||||
.update(schema.persons)
|
||||
.set({
|
||||
...updatePersonDto,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.persons.id, id))
|
||||
.returning();
|
||||
|
||||
if (!person) {
|
||||
throw new NotFoundException(`Person with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return person;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a person
|
||||
*/
|
||||
async remove(id: string) {
|
||||
const [person] = await this.db
|
||||
.delete(schema.persons)
|
||||
.where(eq(schema.persons.id, id))
|
||||
.returning();
|
||||
|
||||
if (!person) {
|
||||
throw new NotFoundException(`Person with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return person;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find persons by project ID and group ID
|
||||
*/
|
||||
async findByProjectIdAndGroupId(projectId: string, groupId: string) {
|
||||
return this.db
|
||||
.select({
|
||||
person: schema.persons,
|
||||
})
|
||||
.from(schema.persons)
|
||||
.innerJoin(
|
||||
schema.personToGroup,
|
||||
and(
|
||||
eq(schema.persons.id, schema.personToGroup.personId),
|
||||
eq(schema.personToGroup.groupId, groupId)
|
||||
)
|
||||
)
|
||||
.where(eq(schema.persons.projectId, projectId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a person to a group
|
||||
*/
|
||||
async addToGroup(personId: string, groupId: string) {
|
||||
const [relation] = await this.db
|
||||
.insert(schema.personToGroup)
|
||||
.values({
|
||||
personId,
|
||||
groupId,
|
||||
})
|
||||
.returning();
|
||||
return relation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a person from a group
|
||||
*/
|
||||
async removeFromGroup(personId: string, groupId: string) {
|
||||
const [relation] = await this.db
|
||||
.delete(schema.personToGroup)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.personToGroup.personId, personId),
|
||||
eq(schema.personToGroup.groupId, groupId)
|
||||
)
|
||||
)
|
||||
.returning();
|
||||
|
||||
if (!relation) {
|
||||
throw new NotFoundException(`Person with ID ${personId} not found in group with ID ${groupId}`);
|
||||
}
|
||||
|
||||
return relation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ProjectsService } from '../services/projects.service';
|
||||
import { CreateProjectDto } from '../dto/create-project.dto';
|
||||
import { UpdateProjectDto } from '../dto/update-project.dto';
|
||||
|
||||
@Controller('projects')
|
||||
export class ProjectsController {
|
||||
constructor(private readonly projectsService: ProjectsService) {}
|
||||
|
||||
/**
|
||||
* Create a new project
|
||||
*/
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
create(@Body() createProjectDto: CreateProjectDto) {
|
||||
return this.projectsService.create(createProjectDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all projects or filter by owner ID
|
||||
*/
|
||||
@Get()
|
||||
findAll(@Query('ownerId') ownerId?: string) {
|
||||
if (ownerId) {
|
||||
return this.projectsService.findByOwnerId(ownerId);
|
||||
}
|
||||
return this.projectsService.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a project by ID
|
||||
*/
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.projectsService.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a project
|
||||
*/
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() updateProjectDto: UpdateProjectDto) {
|
||||
return this.projectsService.update(id, updateProjectDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a project
|
||||
*/
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
remove(@Param('id') id: string) {
|
||||
return this.projectsService.remove(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has access to a project
|
||||
*/
|
||||
@Get(':id/check-access/:userId')
|
||||
checkUserAccess(@Param('id') id: string, @Param('userId') userId: string) {
|
||||
return this.projectsService.checkUserAccess(id, userId);
|
||||
}
|
||||
}
|
||||
22
backend/src/modules/projects/dto/create-project.dto.ts
Normal file
22
backend/src/modules/projects/dto/create-project.dto.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { IsString, IsNotEmpty, IsOptional, IsObject, IsUUID } from 'class-validator';
|
||||
|
||||
/**
|
||||
* DTO for creating a new project
|
||||
*/
|
||||
export class CreateProjectDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
ownerId: string;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
settings?: Record<string, any>;
|
||||
}
|
||||
22
backend/src/modules/projects/dto/update-project.dto.ts
Normal file
22
backend/src/modules/projects/dto/update-project.dto.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { IsString, IsOptional, IsObject, IsUUID } from 'class-validator';
|
||||
|
||||
/**
|
||||
* DTO for updating a project
|
||||
*/
|
||||
export class UpdateProjectDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
ownerId?: string;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
settings?: Record<string, any>;
|
||||
}
|
||||
10
backend/src/modules/projects/projects.module.ts
Normal file
10
backend/src/modules/projects/projects.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ProjectsController } from './controllers/projects.controller';
|
||||
import { ProjectsService } from './services/projects.service';
|
||||
|
||||
@Module({
|
||||
controllers: [ProjectsController],
|
||||
providers: [ProjectsService],
|
||||
exports: [ProjectsService],
|
||||
})
|
||||
export class ProjectsModule {}
|
||||
108
backend/src/modules/projects/services/projects.service.ts
Normal file
108
backend/src/modules/projects/services/projects.service.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Injectable, NotFoundException, Inject } from '@nestjs/common';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { DRIZZLE } from '../../../database/database.module';
|
||||
import * as schema from '../../../database/schema';
|
||||
import { CreateProjectDto } from '../dto/create-project.dto';
|
||||
import { UpdateProjectDto } from '../dto/update-project.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ProjectsService {
|
||||
constructor(@Inject(DRIZZLE) private readonly db: any) {}
|
||||
|
||||
/**
|
||||
* Create a new project
|
||||
*/
|
||||
async create(createProjectDto: CreateProjectDto) {
|
||||
const [project] = await this.db
|
||||
.insert(schema.projects)
|
||||
.values(createProjectDto)
|
||||
.returning();
|
||||
return project;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all projects
|
||||
*/
|
||||
async findAll() {
|
||||
return this.db.select().from(schema.projects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find projects by owner ID
|
||||
*/
|
||||
async findByOwnerId(ownerId: string) {
|
||||
return this.db
|
||||
.select()
|
||||
.from(schema.projects)
|
||||
.where(eq(schema.projects.ownerId, ownerId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a project by ID
|
||||
*/
|
||||
async findById(id: string) {
|
||||
const [project] = await this.db
|
||||
.select()
|
||||
.from(schema.projects)
|
||||
.where(eq(schema.projects.id, id));
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundException(`Project with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a project
|
||||
*/
|
||||
async update(id: string, updateProjectDto: UpdateProjectDto) {
|
||||
const [project] = await this.db
|
||||
.update(schema.projects)
|
||||
.set({
|
||||
...updateProjectDto,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.projects.id, id))
|
||||
.returning();
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundException(`Project with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a project
|
||||
*/
|
||||
async remove(id: string) {
|
||||
const [project] = await this.db
|
||||
.delete(schema.projects)
|
||||
.where(eq(schema.projects.id, id))
|
||||
.returning();
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundException(`Project with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has access to a project
|
||||
*/
|
||||
async checkUserAccess(projectId: string, userId: string) {
|
||||
const [project] = await this.db
|
||||
.select()
|
||||
.from(schema.projects)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.projects.id, projectId),
|
||||
eq(schema.projects.ownerId, userId)
|
||||
)
|
||||
);
|
||||
|
||||
return !!project;
|
||||
}
|
||||
}
|
||||
77
backend/src/modules/users/controllers/users.controller.ts
Normal file
77
backend/src/modules/users/controllers/users.controller.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { UsersService } from '../services/users.service';
|
||||
import { CreateUserDto } from '../dto/create-user.dto';
|
||||
import { UpdateUserDto } from '../dto/update-user.dto';
|
||||
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
/**
|
||||
* Create a new user
|
||||
*/
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
create(@Body() createUserDto: CreateUserDto) {
|
||||
return this.usersService.create(createUserDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users
|
||||
*/
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.usersService.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by ID
|
||||
*/
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.usersService.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user
|
||||
*/
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
|
||||
return this.usersService.update(id, updateUserDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user
|
||||
*/
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
remove(@Param('id') id: string) {
|
||||
return this.usersService.remove(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update GDPR consent timestamp
|
||||
*/
|
||||
@Post(':id/gdpr-consent')
|
||||
updateGdprConsent(@Param('id') id: string) {
|
||||
return this.usersService.updateGdprConsent(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export user data (for GDPR compliance)
|
||||
*/
|
||||
@Get(':id/export-data')
|
||||
exportUserData(@Param('id') id: string) {
|
||||
return this.usersService.exportUserData(id);
|
||||
}
|
||||
}
|
||||
22
backend/src/modules/users/dto/create-user.dto.ts
Normal file
22
backend/src/modules/users/dto/create-user.dto.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { IsString, IsNotEmpty, IsOptional, IsObject } from 'class-validator';
|
||||
|
||||
/**
|
||||
* DTO for creating a new user
|
||||
*/
|
||||
export class CreateUserDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
avatar?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
githubId: string;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
28
backend/src/modules/users/dto/update-user.dto.ts
Normal file
28
backend/src/modules/users/dto/update-user.dto.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { IsString, IsOptional, IsObject, IsDate } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
/**
|
||||
* DTO for updating a user
|
||||
*/
|
||||
export class UpdateUserDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
avatar?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
githubId?: string;
|
||||
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
@Type(() => Date)
|
||||
gdprTimestamp?: Date;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
119
backend/src/modules/users/services/users.service.ts
Normal file
119
backend/src/modules/users/services/users.service.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Injectable, NotFoundException, Inject } from '@nestjs/common';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { DRIZZLE } from '../../../database/database.module';
|
||||
import * as schema from '../../../database/schema';
|
||||
import { CreateUserDto } from '../dto/create-user.dto';
|
||||
import { UpdateUserDto } from '../dto/update-user.dto';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(@Inject(DRIZZLE) private readonly db: any) {}
|
||||
|
||||
/**
|
||||
* Create a new user
|
||||
*/
|
||||
async create(createUserDto: CreateUserDto) {
|
||||
const [user] = await this.db
|
||||
.insert(schema.users)
|
||||
.values({
|
||||
...createUserDto,
|
||||
gdprTimestamp: new Date(),
|
||||
})
|
||||
.returning();
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all users
|
||||
*/
|
||||
async findAll() {
|
||||
return this.db.select().from(schema.users);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a user by ID
|
||||
*/
|
||||
async findById(id: string) {
|
||||
const [user] = await this.db
|
||||
.select()
|
||||
.from(schema.users)
|
||||
.where(eq(schema.users.id, id));
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a user by GitHub ID
|
||||
*/
|
||||
async findByGithubId(githubId: string) {
|
||||
const [user] = await this.db
|
||||
.select()
|
||||
.from(schema.users)
|
||||
.where(eq(schema.users.githubId, githubId));
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user
|
||||
*/
|
||||
async update(id: string, updateUserDto: UpdateUserDto) {
|
||||
const [user] = await this.db
|
||||
.update(schema.users)
|
||||
.set({
|
||||
...updateUserDto,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.users.id, id))
|
||||
.returning();
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user
|
||||
*/
|
||||
async remove(id: string) {
|
||||
const [user] = await this.db
|
||||
.delete(schema.users)
|
||||
.where(eq(schema.users.id, id))
|
||||
.returning();
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update GDPR consent timestamp
|
||||
*/
|
||||
async updateGdprConsent(id: string) {
|
||||
return this.update(id, { gdprTimestamp: new Date() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Export user data (for GDPR compliance)
|
||||
*/
|
||||
async exportUserData(id: string) {
|
||||
const user = await this.findById(id);
|
||||
const projects = await this.db
|
||||
.select()
|
||||
.from(schema.projects)
|
||||
.where(eq(schema.projects.ownerId, id));
|
||||
|
||||
return {
|
||||
user,
|
||||
projects,
|
||||
};
|
||||
}
|
||||
}
|
||||
10
backend/src/modules/users/users.module.ts
Normal file
10
backend/src/modules/users/users.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersController } from './controllers/users.controller';
|
||||
import { UsersService } from './services/users.service';
|
||||
|
||||
@Module({
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
Reference in New Issue
Block a user