feat: Implement admin user management features
- Added adminCreateUser endpoint and corresponding DTO for creating users. - Implemented deleteUser and resetPassword functionalities for admin users. - Introduced updateEmail functionality for admin users. - Updated UsersService to handle user creation, deletion, password reset, and email updates. - Modified UsersController to include new admin routes with appropriate role checks. - Refactored frontend navigation to link to user management under profile. - Created new profile tabs for user management and database management. - Developed AnvandareClient component for user management, including user creation, deletion, role changes, and password resets. - Added DatabsTab for managing product listings and merging duplicates. - Enhanced MinProfilTab for user profile management with form handling.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Controller, Get, Patch, Body, Param, ParseIntPipe, BadRequestException } from '@nestjs/common';
|
||||
import { IsEmail, IsIn, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
import { Controller, Get, Patch, Post, Delete, Body, Param, ParseIntPipe, BadRequestException } from '@nestjs/common';
|
||||
import { IsEmail, IsIn, IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
|
||||
import { UsersService } from './users.service';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { Roles } from '../auth/decorators/roles.decorator';
|
||||
@@ -9,6 +9,29 @@ class SetRoleDto {
|
||||
role: string;
|
||||
}
|
||||
|
||||
class AdminCreateUserDto {
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(50)
|
||||
username: string;
|
||||
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
password: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['admin', 'user'])
|
||||
role?: string;
|
||||
}
|
||||
|
||||
class UpdateEmailDto {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
}
|
||||
|
||||
class UpdateProfileDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@@ -74,4 +97,55 @@ export class UsersController {
|
||||
const updated = await this.usersService.setRole(id, dto.role);
|
||||
return { id: updated.id, username: updated.username, role: updated.role };
|
||||
}
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Post()
|
||||
async adminCreateUser(
|
||||
@Body() dto: AdminCreateUserDto,
|
||||
) {
|
||||
const user = await this.usersService.adminCreate(dto);
|
||||
return { id: user.id, username: user.username, email: user.email, role: user.role, createdAt: user.createdAt };
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Delete(':id')
|
||||
async deleteUser(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@CurrentUser() caller: { userId: number },
|
||||
) {
|
||||
if (caller.userId === id) throw new BadRequestException('Du kan inte ta bort ditt eget konto');
|
||||
await this.usersService.deleteUser(id);
|
||||
return { deleted: true };
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Post(':id/reset-password')
|
||||
async resetPassword(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@CurrentUser() caller: { userId: number },
|
||||
) {
|
||||
if (caller.userId === id) throw new BadRequestException('Du kan inte återställa ditt eget lösenord härifrån');
|
||||
const user = await this.usersService.findById(id);
|
||||
if (!user) throw new BadRequestException('Användaren hittades inte');
|
||||
const { temporaryPassword } = await this.usersService.resetPassword(id);
|
||||
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'appen';
|
||||
const displayName = user.firstName ? user.firstName : user.username;
|
||||
return {
|
||||
to: user.email,
|
||||
subject: 'Ditt lösenord har återställts',
|
||||
body: `Hej ${displayName},\n\nDitt lösenord har återställts av en administratör.\nDitt nya tillôlliga lösenord är: ${temporaryPassword}\n\nLogga in på ${appUrl} och byt lösenord snarast.\n\nHälsningar`,
|
||||
temporaryPassword,
|
||||
};
|
||||
}
|
||||
|
||||
@Roles('admin')
|
||||
@Patch(':id/email')
|
||||
async updateEmail(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@CurrentUser() caller: { userId: number },
|
||||
@Body() dto: UpdateEmailDto,
|
||||
) {
|
||||
if (caller.userId === id) throw new BadRequestException('Använd "Min profil" för att ändra din egen e-post');
|
||||
const updated = await this.usersService.updateEmail(id, dto.email);
|
||||
return { id: updated.id, username: updated.username, email: updated.email };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, ConflictException } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
@@ -31,4 +33,35 @@ export class UsersService {
|
||||
setRole(id: number, role: string) {
|
||||
return this.prisma.user.update({ where: { id }, data: { role } });
|
||||
}
|
||||
|
||||
async adminCreate(data: { username: string; email: string; password: string; role?: string }) {
|
||||
const existing = await this.prisma.user.findFirst({
|
||||
where: { OR: [{ username: data.username }, { email: data.email }] },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException(
|
||||
existing.username === data.username ? 'Användarnamnet är redan taget' : 'E-postadressen används redan',
|
||||
);
|
||||
}
|
||||
const passwordHash = await bcrypt.hash(data.password, 12);
|
||||
return this.prisma.user.create({
|
||||
data: { username: data.username, email: data.email, passwordHash, role: data.role ?? 'user' },
|
||||
});
|
||||
}
|
||||
|
||||
deleteUser(id: number) {
|
||||
return this.prisma.user.delete({ where: { id } });
|
||||
}
|
||||
|
||||
async resetPassword(id: number): Promise<{ temporaryPassword: string }> {
|
||||
// Generera läsbart 12-teckens lösenord (4 ord från slumpmässiga bytes)
|
||||
const temporaryPassword = crypto.randomBytes(9).toString('base64url').slice(0, 12);
|
||||
const passwordHash = await bcrypt.hash(temporaryPassword, 12);
|
||||
await this.prisma.user.update({ where: { id }, data: { passwordHash } });
|
||||
return { temporaryPassword };
|
||||
}
|
||||
|
||||
updateEmail(id: number, email: string) {
|
||||
return this.prisma.user.update({ where: { id }, data: { email } });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user