diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 0f5d7ef..0065d2f 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,13 +4,10 @@
-
-
-
-
+
-
-
+
+
@@ -132,7 +129,7 @@
1774643312599
-
+
@@ -190,7 +187,15 @@
1774709613724
-
+
+
+ 1774710366017
+
+
+
+ 1774710366017
+
+
@@ -204,6 +209,7 @@
-
+
+
\ No newline at end of file
diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts
index 7f8a80c..25f2e9c 100644
--- a/apps/api/src/app.module.ts
+++ b/apps/api/src/app.module.ts
@@ -8,6 +8,7 @@ import { AuthModule } from './auth/auth.module';
import { ReservationsModule } from './reservations/reservations.module';
import { EventsModule } from './events/events.module';
import { ReportsModule } from './reports/reports.module';
+import { UsersModule } from './users/users.module';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
@Module({
@@ -19,6 +20,7 @@ import { LoggerMiddleware } from './common/middleware/logger.middleware';
ReservationsModule,
EventsModule,
ReportsModule,
+ UsersModule,
],
controllers: [AppController],
providers: [AppService],
diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts
index eda8ad6..422c52a 100644
--- a/apps/api/src/auth/auth.controller.ts
+++ b/apps/api/src/auth/auth.controller.ts
@@ -1,6 +1,7 @@
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
+import { RegisterDto } from './dto/register.dto';
@Controller('auth')
export class AuthController {
@@ -10,4 +11,9 @@ export class AuthController {
async login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
+
+ @Post('register')
+ async register(@Body() registerDto: RegisterDto) {
+ return this.authService.register(registerDto);
+ }
}
diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts
index b52f5e7..feb0b21 100644
--- a/apps/api/src/auth/auth.service.ts
+++ b/apps/api/src/auth/auth.service.ts
@@ -1,8 +1,9 @@
-import { Injectable, UnauthorizedException } from '@nestjs/common';
+import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from '../prisma/prisma.service';
import * as bcrypt from 'bcrypt';
import { LoginDto } from './dto/login.dto';
+import { RegisterDto } from './dto/register.dto';
@Injectable()
export class AuthService {
@@ -35,4 +36,39 @@ export class AuthService {
}
throw new UnauthorizedException('Invalid credentials');
}
+
+ async register(registerDto: RegisterDto) {
+ try {
+ const payload = this.jwtService.verify(registerDto.token);
+ if (payload.type !== 'INVITE') {
+ throw new BadRequestException('Invalid invite token');
+ }
+
+ const existing = await this.prisma.user.findUnique({
+ where: { email: registerDto.email },
+ });
+ if (existing) {
+ throw new BadRequestException('Email already registered');
+ }
+
+ const hashedPassword = await bcrypt.hash(registerDto.password, 10);
+ const user = await this.prisma.user.create({
+ data: {
+ email: registerDto.email,
+ password: hashedPassword,
+ role: payload.role,
+ organizationId: payload.organizationId,
+ },
+ });
+
+ return {
+ id: user.id,
+ email: user.email,
+ role: user.role,
+ };
+ } catch (e) {
+ if (e instanceof BadRequestException) throw e;
+ throw new BadRequestException('Invalid or expired invite token');
+ }
+ }
}
diff --git a/apps/api/src/auth/dto/register.dto.ts b/apps/api/src/auth/dto/register.dto.ts
new file mode 100644
index 0000000..a13b2e7
--- /dev/null
+++ b/apps/api/src/auth/dto/register.dto.ts
@@ -0,0 +1,16 @@
+import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
+
+export class RegisterDto {
+ @IsEmail()
+ @IsNotEmpty()
+ email: string;
+
+ @IsString()
+ @IsNotEmpty()
+ @MinLength(6)
+ password: string;
+
+ @IsString()
+ @IsNotEmpty()
+ token: string;
+}
diff --git a/apps/api/src/users/dto/update-user.dto.ts b/apps/api/src/users/dto/update-user.dto.ts
new file mode 100644
index 0000000..8eb8d08
--- /dev/null
+++ b/apps/api/src/users/dto/update-user.dto.ts
@@ -0,0 +1,17 @@
+import { IsEmail, IsEnum, IsOptional, IsString, MinLength } from 'class-validator';
+import { Role } from '@prisma/client';
+
+export class UpdateUserDto {
+ @IsEmail()
+ @IsOptional()
+ email?: string;
+
+ @IsEnum(Role)
+ @IsOptional()
+ role?: Role;
+
+ @IsString()
+ @MinLength(6)
+ @IsOptional()
+ password?: string;
+}
diff --git a/apps/api/src/users/users.controller.ts b/apps/api/src/users/users.controller.ts
new file mode 100644
index 0000000..66d0f87
--- /dev/null
+++ b/apps/api/src/users/users.controller.ts
@@ -0,0 +1,34 @@
+import { Controller, Get, Post, Patch, Body, Param, UseGuards, Req } from '@nestjs/common';
+import { UsersService } from './users.service';
+import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
+import { RolesGuard } from '../auth/guards/roles.guard';
+import { Roles } from '../auth/decorators/roles.decorator';
+import { Role } from '@prisma/client';
+import { UpdateUserDto } from './dto/update-user.dto';
+
+@Controller('users')
+@UseGuards(JwtAuthGuard, RolesGuard)
+@Roles(Role.ADMIN)
+export class UsersController {
+ constructor(private readonly usersService: UsersService) {}
+
+ @Get()
+ findAll(@Req() req) {
+ return this.usersService.findAll(req.user.organizationId);
+ }
+
+ @Get(':id')
+ findOne(@Param('id') id: string, @Req() req) {
+ return this.usersService.findOne(id, req.user.organizationId);
+ }
+
+ @Patch(':id')
+ update(@Param('id') id: string, @Req() req, @Body() updateUserDto: UpdateUserDto) {
+ return this.usersService.update(id, req.user.organizationId, updateUserDto);
+ }
+
+ @Post('invite')
+ createInvite(@Req() req, @Body('role') role?: string) {
+ return this.usersService.createInvite(req.user.organizationId, role);
+ }
+}
diff --git a/apps/api/src/users/users.module.ts b/apps/api/src/users/users.module.ts
new file mode 100644
index 0000000..a2212dc
--- /dev/null
+++ b/apps/api/src/users/users.module.ts
@@ -0,0 +1,22 @@
+import { Module } from '@nestjs/common';
+import { UsersService } from './users.service';
+import { UsersController } from './users.controller';
+import { JwtModule } from '@nestjs/jwt';
+import { ConfigModule, ConfigService } from '@nestjs/config';
+
+@Module({
+ imports: [
+ JwtModule.registerAsync({
+ imports: [ConfigModule],
+ inject: [ConfigService],
+ useFactory: (config: ConfigService) => ({
+ secret: config.get('JWT_SECRET') || 'secret',
+ signOptions: { expiresIn: '7d' },
+ }),
+ }),
+ ],
+ providers: [UsersService],
+ controllers: [UsersController],
+ exports: [UsersService],
+})
+export class UsersModule {}
diff --git a/apps/api/src/users/users.service.ts b/apps/api/src/users/users.service.ts
new file mode 100644
index 0000000..9d10247
--- /dev/null
+++ b/apps/api/src/users/users.service.ts
@@ -0,0 +1,72 @@
+import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
+import { PrismaService } from '../prisma/prisma.service';
+import { UpdateUserDto } from './dto/update-user.dto';
+import * as bcrypt from 'bcrypt';
+import { JwtService } from '@nestjs/jwt';
+
+@Injectable()
+export class UsersService {
+ constructor(
+ private prisma: PrismaService,
+ private jwtService: JwtService,
+ ) {}
+
+ async findAll(organizationId: string) {
+ return this.prisma.user.findMany({
+ where: { organizationId },
+ select: {
+ id: true,
+ email: true,
+ role: true,
+ createdAt: true,
+ updatedAt: true,
+ },
+ orderBy: { email: 'asc' },
+ });
+ }
+
+ async findOne(id: string, organizationId: string) {
+ const user = await this.prisma.user.findFirst({
+ where: { id, organizationId },
+ select: {
+ id: true,
+ email: true,
+ role: true,
+ createdAt: true,
+ updatedAt: true,
+ },
+ });
+
+ if (!user) {
+ throw new NotFoundException(`User with ID ${id} not found`);
+ }
+
+ return user;
+ }
+
+ async update(id: string, organizationId: string, updateUserDto: UpdateUserDto) {
+ const user = await this.findOne(id, organizationId);
+
+ const data: any = { ...updateUserDto };
+ if (data.password) {
+ data.password = await bcrypt.hash(data.password, 10);
+ }
+
+ return this.prisma.user.update({
+ where: { id: user.id },
+ data,
+ select: {
+ id: true,
+ email: true,
+ role: true,
+ updatedAt: true,
+ },
+ });
+ }
+
+ async createInvite(organizationId: string, role: string = 'EMPLOYEE') {
+ const payload = { organizationId, role, type: 'INVITE' };
+ const token = this.jwtService.sign(payload, { expiresIn: '7d' });
+ return { token };
+ }
+}
diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte
index 19747f6..f344386 100644
--- a/apps/web/src/routes/+layout.svelte
+++ b/apps/web/src/routes/+layout.svelte
@@ -14,6 +14,7 @@
{#if $token}
{#if $user?.role === 'ADMIN'}
+
Users
Reports
{/if}
Dashboard
diff --git a/apps/web/src/routes/admin/users/+page.svelte b/apps/web/src/routes/admin/users/+page.svelte
new file mode 100644
index 0000000..68625ee
--- /dev/null
+++ b/apps/web/src/routes/admin/users/+page.svelte
@@ -0,0 +1,226 @@
+
+
+
+
+
User Management
+
+
+
+
+
+
+ {#if loading}
+
+ {:else if error}
+
+ {error}
+
+ {:else}
+
+
+
+
+ | User |
+ Role |
+ Joined |
+ Actions |
+
+
+
+ {#each users as user}
+
+ |
+ {user.email}
+ ID: {user.id.substring(0, 8)}...
+ |
+
+
+ |
+
+ {new Date(user.createdAt).toLocaleDateString()}
+ |
+
+
+ |
+
+ {/each}
+
+
+
+ {/if}
+
+
+
+{#if showInviteModal}
+
+
+
Invitation Link Generated
+
Anyone with this link can register as a {inviteRole} in your organization.
+
+
+
+
+
+
+
+
+
+
+
+{/if}
+
+
+{#if showPasswordModal}
+
+
+
Change Password
+
Set a new password for {selectedUser?.email}
+
+
+
+
+
+
+
+
+
+{/if}
diff --git a/apps/web/src/routes/register/+page.svelte b/apps/web/src/routes/register/+page.svelte
new file mode 100644
index 0000000..c15f106
--- /dev/null
+++ b/apps/web/src/routes/register/+page.svelte
@@ -0,0 +1,159 @@
+
+
+
+
+
+ H
+
+
+
+ Create your account using the invitation
+
+
+
+
+
+ {#if success}
+
+
+
+
+
+ Registration successful! Redirecting to login...
+
+
+
+
+ {:else}
+ {#if error}
+
+ {/if}
+
+
+ {/if}
+
+
+