* feat(logging): add PV_LOG_LEVEL environment variable to set backend log level
* feat(ldap): Adding a more verbose logging output to debug LDAP issues
* fix(ldap): fixed user logins with special characters within the users dn by switching to ldapts
* feat(ldap): made the member of and email attribute names configurable
* fix(ldap): properly handle email like usernames and fixing #601
* Revert "fix: disable email login if ldap is enabled"
This reverts commit d9cfe697d6.
* feat(ldap): disable the ability for a user to change his email when it's a LDAP user
* feat(ldap): relaxed username pattern by allowing the @ character in usernames
196 lines
6.2 KiB
TypeScript
196 lines
6.2 KiB
TypeScript
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
|
import * as argon from "argon2";
|
|
import * as crypto from "crypto";
|
|
import { EmailService } from "src/email/email.service";
|
|
import { PrismaService } from "src/prisma/prisma.service";
|
|
import { FileService } from "../file/file.service";
|
|
import { CreateUserDTO } from "./dto/createUser.dto";
|
|
import { UpdateUserDto } from "./dto/updateUser.dto";
|
|
import { ConfigService } from "../config/config.service";
|
|
import { Entry } from "ldapts";
|
|
import { AuthSignInDTO } from "src/auth/dto/authSignIn.dto";
|
|
import { inspect } from "util";
|
|
|
|
@Injectable()
|
|
export class UserSevice {
|
|
private readonly logger = new Logger(UserSevice.name);
|
|
|
|
constructor(
|
|
private prisma: PrismaService,
|
|
private emailService: EmailService,
|
|
private fileService: FileService,
|
|
private configService: ConfigService,
|
|
) { }
|
|
|
|
async list() {
|
|
return await this.prisma.user.findMany();
|
|
}
|
|
|
|
async get(id: string) {
|
|
return await this.prisma.user.findUnique({ where: { id } });
|
|
}
|
|
|
|
async create(dto: CreateUserDTO) {
|
|
let hash: string;
|
|
|
|
// The password can be undefined if the user is invited by an admin
|
|
if (!dto.password) {
|
|
const randomPassword = crypto.randomUUID();
|
|
hash = await argon.hash(randomPassword);
|
|
await this.emailService.sendInviteEmail(dto.email, randomPassword);
|
|
} else {
|
|
hash = await argon.hash(dto.password);
|
|
}
|
|
|
|
try {
|
|
return await this.prisma.user.create({
|
|
data: {
|
|
...dto,
|
|
password: hash,
|
|
},
|
|
});
|
|
} catch (e) {
|
|
if (e instanceof PrismaClientKnownRequestError) {
|
|
if (e.code == "P2002") {
|
|
const duplicatedField: string = e.meta.target[0];
|
|
throw new BadRequestException(
|
|
`A user with this ${duplicatedField} already exists`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async update(id: string, user: UpdateUserDto) {
|
|
try {
|
|
const hash = user.password && (await argon.hash(user.password));
|
|
|
|
return await this.prisma.user.update({
|
|
where: { id },
|
|
data: { ...user, password: hash },
|
|
});
|
|
} catch (e) {
|
|
if (e instanceof PrismaClientKnownRequestError) {
|
|
if (e.code == "P2002") {
|
|
const duplicatedField: string = e.meta.target[0];
|
|
throw new BadRequestException(
|
|
`A user with this ${duplicatedField} already exists`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async delete(id: string) {
|
|
const user = await this.prisma.user.findUnique({
|
|
where: { id },
|
|
include: { shares: true },
|
|
});
|
|
if (!user) throw new BadRequestException("User not found");
|
|
|
|
await Promise.all(
|
|
user.shares.map((share) => this.fileService.deleteAllFiles(share.id)),
|
|
);
|
|
|
|
return await this.prisma.user.delete({ where: { id } });
|
|
}
|
|
|
|
async findOrCreateFromLDAP(providedCredentials: AuthSignInDTO, ldapEntry: Entry) {
|
|
const fieldNameMemberOf = this.configService.get("ldap.fieldNameMemberOf");
|
|
const fieldNameEmail = this.configService.get("ldap.fieldNameEmail");
|
|
|
|
let isAdmin = false;
|
|
if (fieldNameMemberOf in ldapEntry) {
|
|
const adminGroup = this.configService.get("ldap.adminGroups");
|
|
const entryGroups = Array.isArray(ldapEntry[fieldNameMemberOf]) ? ldapEntry[fieldNameMemberOf] : [ldapEntry[fieldNameMemberOf]];
|
|
isAdmin = entryGroups.includes(adminGroup) ?? false;
|
|
} else {
|
|
this.logger.warn(`Trying to create/update a ldap user but the member field ${fieldNameMemberOf} is not present.`);
|
|
}
|
|
|
|
let userEmail: string | null = null;
|
|
if (fieldNameEmail in ldapEntry) {
|
|
const value = Array.isArray(ldapEntry[fieldNameEmail]) ? ldapEntry[fieldNameEmail][0] : ldapEntry[fieldNameEmail];
|
|
if (value) {
|
|
userEmail = value.toString();
|
|
}
|
|
} else {
|
|
this.logger.warn(`Trying to create/update a ldap user but the email field ${fieldNameEmail} is not present.`);
|
|
}
|
|
|
|
if (providedCredentials.email) {
|
|
/* if LDAP does not provides an users email address, take the user provided email address instead */
|
|
userEmail = providedCredentials.email;
|
|
}
|
|
|
|
const randomId = crypto.randomUUID();
|
|
const placeholderUsername = `ldap_user_${randomId}`;
|
|
const placeholderEMail = `${randomId}@ldap.local`;
|
|
|
|
try {
|
|
const user = await this.prisma.user.upsert({
|
|
create: {
|
|
username: providedCredentials.username ?? placeholderUsername,
|
|
email: userEmail ?? placeholderEMail,
|
|
password: await argon.hash(crypto.randomUUID()),
|
|
|
|
isAdmin,
|
|
ldapDN: ldapEntry.dn,
|
|
},
|
|
update: {
|
|
isAdmin,
|
|
ldapDN: ldapEntry.dn,
|
|
},
|
|
where: {
|
|
ldapDN: ldapEntry.dn,
|
|
},
|
|
});
|
|
|
|
if (user.username === placeholderUsername) {
|
|
/* Give the user a human readable name if the user has been created with a placeholder username */
|
|
await this.prisma.user.update({
|
|
where: {
|
|
id: user.id,
|
|
},
|
|
data: {
|
|
username: `user_${user.id}`
|
|
}
|
|
}).then(newUser => {
|
|
user.username = newUser.username;
|
|
}).catch(error => {
|
|
this.logger.warn(`Failed to update users ${user.id} placeholder username: ${inspect(error)}`);
|
|
});
|
|
}
|
|
|
|
if (userEmail && userEmail !== user.email) {
|
|
/* Sync users email if it has changed */
|
|
await this.prisma.user.update({
|
|
where: {
|
|
id: user.id,
|
|
},
|
|
data: {
|
|
email: userEmail
|
|
}
|
|
}).then(newUser => {
|
|
this.logger.log(`Updated users ${user.id} email from ldap from ${user.email} to ${userEmail}.`);
|
|
user.email = newUser.email;
|
|
}).catch(error => {
|
|
this.logger.error(`Failed to update users ${user.id} email to ${userEmail}: ${inspect(error)}`);
|
|
});
|
|
}
|
|
|
|
return user;
|
|
} catch (e) {
|
|
if (e instanceof PrismaClientKnownRequestError) {
|
|
if (e.code == "P2002") {
|
|
const duplicatedField: string = e.meta.target[0];
|
|
throw new BadRequestException(
|
|
`A user with this ${duplicatedField} already exists`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|