You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-08-10 23:22:22 +02:00
Add web interface with admin functionality (#167)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120
|
||||
"printWidth": 120,
|
||||
"semi": true
|
||||
}
|
||||
|
@@ -7,16 +7,16 @@ import { SignUpDto } from './dto/sign-up.dto';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
constructor(private readonly authService: AuthService) { }
|
||||
|
||||
@Post('/login')
|
||||
async login(@Body(ValidationPipe) loginCredential: LoginCredentialDto) {
|
||||
return await this.authService.login(loginCredential);
|
||||
}
|
||||
|
||||
@Post('/signUp')
|
||||
async signUp(@Body(ValidationPipe) signUpCrendential: SignUpDto) {
|
||||
return await this.authService.signUp(signUpCrendential);
|
||||
@Post('/admin-sign-up')
|
||||
async adminSignUp(@Body(ValidationPipe) signUpCrendential: SignUpDto) {
|
||||
return await this.authService.adminSignUp(signUpCrendential);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
|
@@ -14,12 +14,12 @@ export class AuthService {
|
||||
@InjectRepository(UserEntity)
|
||||
private userRepository: Repository<UserEntity>,
|
||||
private immichJwtService: ImmichJwtService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
private async validateUser(loginCredential: LoginCredentialDto): Promise<UserEntity> {
|
||||
const user = await this.userRepository.findOne(
|
||||
{ email: loginCredential.email },
|
||||
{ select: ['id', 'email', 'password', 'salt'] },
|
||||
{ select: ['id', 'email', 'password', 'salt', 'firstName', 'lastName', 'isAdmin'] },
|
||||
);
|
||||
|
||||
const isAuthenticated = await this.validatePassword(user.password, loginCredential.password, user.salt);
|
||||
@@ -44,32 +44,45 @@ export class AuthService {
|
||||
accessToken: await this.immichJwtService.generateToken(payload),
|
||||
userId: validatedUser.id,
|
||||
userEmail: validatedUser.email,
|
||||
firstName: validatedUser.firstName,
|
||||
lastName: validatedUser.lastName,
|
||||
isAdmin: validatedUser.isAdmin,
|
||||
profileImagePath: validatedUser.profileImagePath,
|
||||
isFirstLogin: validatedUser.isFirstLoggedIn
|
||||
};
|
||||
}
|
||||
|
||||
public async signUp(signUpCrendential: SignUpDto) {
|
||||
const registerUser = await this.userRepository.findOne({ email: signUpCrendential.email });
|
||||
|
||||
if (registerUser) {
|
||||
throw new BadRequestException('User exist');
|
||||
public async adminSignUp(signUpCrendential: SignUpDto) {
|
||||
const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } });
|
||||
|
||||
if (adminUser) {
|
||||
throw new BadRequestException('The server already has an admin')
|
||||
}
|
||||
|
||||
const newUser = new UserEntity();
|
||||
newUser.email = signUpCrendential.email;
|
||||
newUser.salt = await bcrypt.genSalt();
|
||||
newUser.password = await this.hashPassword(signUpCrendential.password, newUser.salt);
|
||||
|
||||
const newAdminUser = new UserEntity();
|
||||
newAdminUser.email = signUpCrendential.email;
|
||||
newAdminUser.salt = await bcrypt.genSalt();
|
||||
newAdminUser.password = await this.hashPassword(signUpCrendential.password, newAdminUser.salt);
|
||||
newAdminUser.firstName = signUpCrendential.firstName;
|
||||
newAdminUser.lastName = signUpCrendential.lastName;
|
||||
newAdminUser.isAdmin = true;
|
||||
|
||||
try {
|
||||
const savedUser = await this.userRepository.save(newUser);
|
||||
const savedNewAdminUserUser = await this.userRepository.save(newAdminUser);
|
||||
|
||||
return {
|
||||
id: savedUser.id,
|
||||
email: savedUser.email,
|
||||
createdAt: savedUser.createdAt,
|
||||
id: savedNewAdminUserUser.id,
|
||||
email: savedNewAdminUserUser.email,
|
||||
firstName: savedNewAdminUserUser.firstName,
|
||||
lastName: savedNewAdminUserUser.lastName,
|
||||
createdAt: savedNewAdminUserUser.createdAt,
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
Logger.error('e', 'signUp');
|
||||
throw new InternalServerErrorException('Failed to register new user');
|
||||
throw new InternalServerErrorException('Failed to register new admin user');
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -6,4 +6,10 @@ export class SignUpDto {
|
||||
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
firstName: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
lastName: string;
|
||||
}
|
||||
|
@@ -1 +1,27 @@
|
||||
export class CreateUserDto {}
|
||||
import { IsNotEmpty, IsOptional } from 'class-validator';
|
||||
|
||||
export class CreateUserDto {
|
||||
@IsNotEmpty()
|
||||
email: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
firstName: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
lastName: string;
|
||||
|
||||
@IsOptional()
|
||||
profileImagePath: string;
|
||||
|
||||
@IsOptional()
|
||||
isAdmin: boolean;
|
||||
|
||||
@IsOptional()
|
||||
isFirstLoggedIn: boolean;
|
||||
|
||||
@IsOptional()
|
||||
id: string;
|
||||
}
|
||||
|
@@ -5,6 +5,15 @@ export class UserEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
firstName: string;
|
||||
|
||||
@Column()
|
||||
lastName: string;
|
||||
|
||||
@Column()
|
||||
isAdmin: boolean;
|
||||
|
||||
@Column()
|
||||
email: string;
|
||||
|
||||
@@ -14,6 +23,12 @@ export class UserEntity {
|
||||
@Column({ select: false })
|
||||
salt: string;
|
||||
|
||||
@Column()
|
||||
profileImagePath: string;
|
||||
|
||||
@Column()
|
||||
isFirstLoggedIn: boolean;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: string;
|
||||
}
|
||||
|
@@ -1,15 +1,38 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, ValidationPipe, Put, Query } from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { AdminRolesGuard } from '../../middlewares/admin-role-guard.middleware';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { boolean } from 'joi';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('user')
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
constructor(private readonly userService: UserService) { }
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get()
|
||||
async getAllUsers(@GetAuthUser() authUser: AuthUserDto) {
|
||||
return await this.userService.getAllUsers(authUser);
|
||||
async getAllUsers(@GetAuthUser() authUser: AuthUserDto, @Query('isAll') isAll: boolean) {
|
||||
return await this.userService.getAllUsers(authUser, isAll);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(AdminRolesGuard)
|
||||
@Post()
|
||||
async createNewUser(@Body(ValidationPipe) createUserDto: CreateUserDto) {
|
||||
return await this.userService.createUser(createUserDto);
|
||||
}
|
||||
|
||||
@Get('/count')
|
||||
async getUserCount(@Query('isAdmin') isAdmin: boolean) {
|
||||
|
||||
return await this.userService.getUserCount(isAdmin);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put()
|
||||
async updateUser(@Body(ValidationPipe) updateUserDto: UpdateUserDto) {
|
||||
return await this.userService.updateUser(updateUserDto)
|
||||
}
|
||||
}
|
||||
|
@@ -3,10 +3,14 @@ import { UserService } from './user.service';
|
||||
import { UserController } from './user.controller';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UserEntity } from './entities/user.entity';
|
||||
import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module';
|
||||
import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { jwtConfig } from '../../config/jwt.config';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([UserEntity])],
|
||||
imports: [TypeOrmModule.forFeature([UserEntity]), ImmichJwtModule, JwtModule.register(jwtConfig)],
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
providers: [UserService, ImmichJwtService],
|
||||
})
|
||||
export class UserModule {}
|
||||
export class UserModule { }
|
||||
|
@@ -1,21 +1,127 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Not, Repository } from 'typeorm';
|
||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { UserEntity } from './entities/user.entity';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
@InjectRepository(UserEntity)
|
||||
private userRepository: Repository<UserEntity>,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
async getAllUsers(authUser: AuthUserDto, isAll: boolean) {
|
||||
|
||||
if (isAll) {
|
||||
return await this.userRepository.find();
|
||||
}
|
||||
|
||||
async getAllUsers(authUser: AuthUserDto) {
|
||||
return await this.userRepository.find({
|
||||
where: { id: Not(authUser.id) },
|
||||
order: {
|
||||
createdAt: 'DESC'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getUserCount(isAdmin: boolean) {
|
||||
let users;
|
||||
|
||||
if (isAdmin) {
|
||||
users = await this.userRepository.find({ where: { isAdmin: true } });
|
||||
} else {
|
||||
users = await this.userRepository.find();
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
userCount: users.length
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async createUser(createUserDto: CreateUserDto) {
|
||||
const user = await this.userRepository.findOne({ where: { email: createUserDto.email } });
|
||||
|
||||
if (user) {
|
||||
throw new BadRequestException('User exists');
|
||||
}
|
||||
|
||||
const newUser = new UserEntity();
|
||||
newUser.email = createUserDto.email;
|
||||
newUser.salt = await bcrypt.genSalt();
|
||||
newUser.password = await this.hashPassword(createUserDto.password, newUser.salt);
|
||||
newUser.firstName = createUserDto.firstName;
|
||||
newUser.lastName = createUserDto.lastName;
|
||||
newUser.isAdmin = false;
|
||||
|
||||
|
||||
try {
|
||||
const savedUser = await this.userRepository.save(newUser);
|
||||
|
||||
return {
|
||||
id: savedUser.id,
|
||||
email: savedUser.email,
|
||||
firstName: savedUser.firstName,
|
||||
lastName: savedUser.lastName,
|
||||
createdAt: savedUser.createdAt,
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
Logger.error(e, 'Create new user');
|
||||
throw new InternalServerErrorException('Failed to register new user');
|
||||
}
|
||||
}
|
||||
|
||||
private async hashPassword(password: string, salt: string): Promise<string> {
|
||||
return bcrypt.hash(password, salt);
|
||||
}
|
||||
|
||||
|
||||
async updateUser(updateUserDto: UpdateUserDto) {
|
||||
const user = await this.userRepository.findOne(updateUserDto.id);
|
||||
|
||||
user.lastName = updateUserDto.lastName || user.lastName;
|
||||
user.firstName = updateUserDto.firstName || user.firstName;
|
||||
user.profileImagePath = updateUserDto.profileImagePath || user.profileImagePath;
|
||||
user.isFirstLoggedIn = updateUserDto.isFirstLoggedIn || user.isFirstLoggedIn;
|
||||
|
||||
// If payload includes password - Create new password for user
|
||||
if (updateUserDto.password) {
|
||||
user.salt = await bcrypt.genSalt();
|
||||
user.password = await this.hashPassword(updateUserDto.password, user.salt);
|
||||
}
|
||||
|
||||
if (updateUserDto.isAdmin) {
|
||||
const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } })
|
||||
|
||||
if (adminUser) {
|
||||
throw new BadRequestException("Admin user exists")
|
||||
}
|
||||
|
||||
user.isAdmin = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedUser = await this.userRepository.save(user);
|
||||
|
||||
return {
|
||||
id: updatedUser.id,
|
||||
email: updatedUser.email,
|
||||
firstName: updatedUser.firstName,
|
||||
lastName: updatedUser.lastName,
|
||||
isAdmin: updatedUser.isAdmin,
|
||||
profileImagePath: updatedUser.profileImagePath,
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
Logger.error(e, 'Create new user');
|
||||
throw new InternalServerErrorException('Failed to register new user');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
server/src/app.controller.ts
Normal file
15
server/src/app.controller.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Controller, Get, Res, Headers } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
|
||||
@Controller()
|
||||
|
||||
export class AppController {
|
||||
constructor() { }
|
||||
|
||||
@Get()
|
||||
async redirectToWebpage(@Res({ passthrough: true }) res: Response, @Headers() headers) {
|
||||
const host = headers.host;
|
||||
|
||||
return res.redirect(`http://${host}:2285`)
|
||||
}
|
||||
}
|
@@ -15,6 +15,7 @@ import { ServerInfoModule } from './api-v1/server-info/server-info.module';
|
||||
import { BackgroundTaskModule } from './modules/background-task/background-task.module';
|
||||
import { CommunicationModule } from './api-v1/communication/communication.module';
|
||||
import { SharingModule } from './api-v1/sharing/sharing.module';
|
||||
import { AppController } from './app.controller';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -44,7 +45,7 @@ import { SharingModule } from './api-v1/sharing/sharing.module';
|
||||
|
||||
SharingModule,
|
||||
],
|
||||
controllers: [],
|
||||
controllers: [AppController],
|
||||
providers: [],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { createParamDecorator, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import { UserEntity } from '../api-v1/user/entities/user.entity';
|
||||
// import { AuthUserDto } from './dto/auth-user.dto';
|
||||
|
||||
@@ -18,4 +18,4 @@ export const GetAuthUser = createParamDecorator((data, ctx: ExecutionContext): A
|
||||
};
|
||||
|
||||
return authUser;
|
||||
});
|
||||
});
|
@@ -7,6 +7,8 @@ import { RedisIoAdapter } from './middlewares/redis-io.adapter.middleware';
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||
|
||||
app.enableCors();
|
||||
|
||||
app.set('trust proxy');
|
||||
|
||||
app.useWebSocketAdapter(new RedisIoAdapter(app));
|
||||
|
30
server/src/middlewares/admin-role-guard.middleware.ts
Normal file
30
server/src/middlewares/admin-role-guard.middleware.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { UserEntity } from '../api-v1/user/entities/user.entity';
|
||||
import { ImmichJwtService } from '../modules/immich-jwt/immich-jwt.service';
|
||||
|
||||
@Injectable()
|
||||
export class AdminRolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector, private jwtService: ImmichJwtService,
|
||||
@InjectRepository(UserEntity)
|
||||
private userRepository: Repository<UserEntity>,
|
||||
) { }
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
if (request.headers['authorization']) {
|
||||
const bearerToken = request.headers['authorization'].split(" ")[1]
|
||||
const { userId } = await this.jwtService.validateToken(bearerToken);
|
||||
|
||||
const user = await this.userRepository.findOne(userId);
|
||||
|
||||
return user.isAdmin;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class UpdateUserTableWithAdminAndName1652633525943 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
alter table users
|
||||
add column if not exists "firstName" varchar default '';
|
||||
|
||||
alter table users
|
||||
add column if not exists "lastName" varchar default '';
|
||||
|
||||
alter table users
|
||||
add column if not exists "profileImagePath" varchar default '';
|
||||
|
||||
alter table users
|
||||
add column if not exists "isAdmin" bool default false;
|
||||
|
||||
alter table users
|
||||
add column if not exists "isFirstLoggedIn" bool default true;
|
||||
`)
|
||||
|
||||
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
alter table users
|
||||
drop column "firstName";
|
||||
|
||||
alter table users
|
||||
drop column "lastName";
|
||||
|
||||
alter table users
|
||||
drop column "isAdmin";
|
||||
|
||||
`);
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user