add invitations
This commit is contained in:
parent
fb4154c9e5
commit
02689f93d1
7
.adminjs/bundle.js
Normal file
7
.adminjs/bundle.js
Normal file
@ -0,0 +1,7 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
AdminJS.UserComponents = {};
|
||||
|
||||
})();
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVuZGxlLmpzIiwic291cmNlcyI6WyJlbnRyeS5qcyJdLCJzb3VyY2VzQ29udGVudCI6WyJBZG1pbkpTLlVzZXJDb21wb25lbnRzID0ge31cbiJdLCJuYW1lcyI6WyJBZG1pbkpTIiwiVXNlckNvbXBvbmVudHMiXSwibWFwcGluZ3MiOiI7OztDQUFBQSxPQUFPLENBQUNDLGNBQWMsR0FBRyxFQUFFOzs7Ozs7In0=
|
1
.adminjs/entry.js
Normal file
1
.adminjs/entry.js
Normal file
@ -0,0 +1 @@
|
||||
AdminJS.UserComponents = {}
|
@ -22,8 +22,8 @@ export class LoginAntispamGuard implements CanActivate {
|
||||
if (['GET', 'OPTIONS'].includes(request.method)) return true;
|
||||
|
||||
const known = await this.iplimit.getAddressLimit(request.ip);
|
||||
if (known && known.attempts > 3) {
|
||||
if (known.attempts > 5) {
|
||||
if (known && known.attempts > 10) {
|
||||
if (known.attempts > 15) {
|
||||
let reported = false;
|
||||
if (!known.reported) {
|
||||
reported = true;
|
||||
@ -36,7 +36,7 @@ export class LoginAntispamGuard implements CanActivate {
|
||||
);
|
||||
}
|
||||
|
||||
const limitMinutes = known.attempts > 10 ? 30 : 10; // Half-Hour
|
||||
const limitMinutes = known.attempts > 15 ? 30 : 10; // Half-Hour
|
||||
await this.iplimit.limitUntil(
|
||||
request.ip,
|
||||
limitMinutes * 60 * 1000,
|
||||
|
@ -67,6 +67,19 @@ export class UserAdminController {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a registraion invitation and send email.
|
||||
* @param body
|
||||
* @returns Success
|
||||
*/
|
||||
@Post('invite')
|
||||
@Scopes('management')
|
||||
@Privileges('admin', 'admin:user')
|
||||
async invite(@Body() { email }: Pick<User, 'email'>) {
|
||||
await this._user.issueRegistrationToken(email);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single user by ID
|
||||
* @param id User ID
|
||||
|
22
src/modules/objects/user/email/invitation.email.ts
Normal file
22
src/modules/objects/user/email/invitation.email.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { EmailTemplate } from 'src/modules/objects/email/email.template';
|
||||
|
||||
export const InvitationEmail = (url: string): EmailTemplate => ({
|
||||
text: `
|
||||
Icy Network
|
||||
|
||||
Please click on the following link to create an account on Icy Network.
|
||||
|
||||
Create your account here: ${url}
|
||||
|
||||
This email was sent to you because you have requested an account on Icy Network. If you did not request this, you may safely ignore this email.
|
||||
`,
|
||||
html: /* html */ `
|
||||
<h1>Icy Network</h1>
|
||||
|
||||
<p><b>Please click on the following link to create an account on Icy Network.</b></p>
|
||||
|
||||
<p>Create your account here: <a href="${url}" target="_blank">${url}</a></p>
|
||||
|
||||
<p>This email was sent to you because you have requested an account on Icy Network. If you did not request this, you may safely ignore this email.</p>
|
||||
`,
|
||||
});
|
@ -12,6 +12,8 @@ Welcome to Icy Network, ${username}!
|
||||
In order to proceed with logging in, please click on the following link to activate your account.
|
||||
|
||||
Activate your account: ${url}
|
||||
|
||||
This email was sent to you because you have created an account on Icy Network. If you did not create an account, you may contact us or just let the account expire.
|
||||
`,
|
||||
html: /* html */ `
|
||||
<h1>Icy Network</h1>
|
||||
@ -21,5 +23,7 @@ Activate your account: ${url}
|
||||
<p>In order to proceed with logging in, please click on the following link to activate your account.</p>
|
||||
|
||||
<p>Activate your account: <a href="${url}" target="_blank">${url}</a></p>
|
||||
|
||||
<p>This email was sent to you because you have created an account on Icy Network. If you did not create an account, you may contact us or just let the account expire.</p>
|
||||
`,
|
||||
});
|
||||
|
@ -6,9 +6,11 @@ import { UploadModule } from '../upload/upload.module';
|
||||
import { UserTokenModule } from '../user-token/user-token.module';
|
||||
import { userProviders } from './user.providers';
|
||||
import { UserService } from './user.service';
|
||||
import { CommonCacheModule } from 'src/modules/cache/cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CommonCacheModule,
|
||||
DatabaseModule,
|
||||
EmailModule,
|
||||
UserTokenModule,
|
||||
|
@ -1,4 +1,9 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
Inject,
|
||||
Injectable,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { ILike, Repository } from 'typeorm';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
@ -12,9 +17,14 @@ import { UserTokenService } from '../user-token/user-token.service';
|
||||
import { ConfigurationService } from 'src/modules/config/config.service';
|
||||
import { Upload } from '../upload/upload.entity';
|
||||
import { UploadService } from '../upload/upload.service';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { Cache } from 'cache-manager';
|
||||
import { InvitationEmail } from './email/invitation.email';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
private readonly logger = new Logger(UserService.name);
|
||||
|
||||
constructor(
|
||||
@Inject('USER_REPOSITORY')
|
||||
private userRepository: Repository<User>,
|
||||
@ -23,6 +33,7 @@ export class UserService {
|
||||
private email: EmailService,
|
||||
private config: ConfigurationService,
|
||||
private upload: UploadService,
|
||||
@Inject(CACHE_MANAGER) private readonly cache: Cache,
|
||||
) {}
|
||||
|
||||
public async getById(id: number, relations?: string[]): Promise<User> {
|
||||
@ -140,12 +151,14 @@ export class UserService {
|
||||
|
||||
user.picture = upload;
|
||||
await this.updateUser(user);
|
||||
this.logger.log(`User ID: ${user.id} avatar has been updated`);
|
||||
return user;
|
||||
}
|
||||
|
||||
public async deleteAvatar(user: User): Promise<void> {
|
||||
if (user.picture) {
|
||||
await this.upload.delete(user.picture);
|
||||
this.logger.log(`User ID: ${user.id} avatar has been deleted`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,6 +178,9 @@ export class UserService {
|
||||
user: User,
|
||||
redirectTo?: string,
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
`Sending an activation email to User ID: ${user.id} (${user.email})`,
|
||||
);
|
||||
const activationToken = await this.userToken.create(
|
||||
user,
|
||||
UserTokenType.ACTIVATION,
|
||||
@ -188,13 +204,22 @@ export class UserService {
|
||||
'Activate your account on Icy Network',
|
||||
content,
|
||||
);
|
||||
this.logger.log(
|
||||
`Sending an activation email to User ID: ${user.id} (${user.email}) was successful.`,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`Sending an activation email to User ID: ${user.id} (${user.email}) failed`,
|
||||
);
|
||||
await this.userToken.delete(activationToken);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public async sendPasswordEmail(user: User): Promise<void> {
|
||||
this.logger.log(
|
||||
`Sending a password reset email to User ID: ${user.id} (${user.email})`,
|
||||
);
|
||||
const passwordToken = await this.userToken.create(
|
||||
user,
|
||||
UserTokenType.PASSWORD,
|
||||
@ -213,7 +238,13 @@ export class UserService {
|
||||
'Reset your password on Icy Network',
|
||||
content,
|
||||
);
|
||||
this.logger.log(
|
||||
`Sent a password reset email to User ID: ${user.id} (${user.email}) successfully`,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`Sending a password reset email to User ID: ${user.id} (${user.email}) failed: ${e}`,
|
||||
);
|
||||
await this.userToken.delete(passwordToken);
|
||||
// silently fail
|
||||
}
|
||||
@ -236,7 +267,12 @@ export class UserService {
|
||||
password: string;
|
||||
},
|
||||
redirectTo?: string,
|
||||
activate = false,
|
||||
): Promise<User> {
|
||||
this.logger.log(
|
||||
`Starting registration of new user ${newUserInfo.username} (${newUserInfo.email})`,
|
||||
);
|
||||
|
||||
if (!!(await this.getByEmail(newUserInfo.email))) {
|
||||
throw new Error('Email is already in use!');
|
||||
}
|
||||
@ -253,18 +289,71 @@ export class UserService {
|
||||
user.display_name = newUserInfo.display_name;
|
||||
user.password = hashword;
|
||||
user.activity_at = new Date();
|
||||
user.activated = activate;
|
||||
|
||||
await this.userRepository.insert(user);
|
||||
await this.userRepository.save(user);
|
||||
|
||||
try {
|
||||
await this.sendActivationEmail(user, redirectTo);
|
||||
} catch (e) {
|
||||
await this.userRepository.remove(user);
|
||||
throw new Error(
|
||||
'Failed to send activation email! Please check your email address and try again!',
|
||||
);
|
||||
if (!user.activated) {
|
||||
try {
|
||||
await this.sendActivationEmail(user, redirectTo);
|
||||
} catch (e) {
|
||||
await this.userRepository.remove(user);
|
||||
throw new Error(
|
||||
'Failed to send activation email! Please check your email address and try again!',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Registered a new user ${newUserInfo.username} (${newUserInfo.email}) ID: ${user.id}`,
|
||||
);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public async issueRegistrationToken(email: string) {
|
||||
this.logger.log(`Issuing a new registration token for ${email}`);
|
||||
|
||||
const existingUser = await this.getByEmail(email);
|
||||
if (existingUser) {
|
||||
throw new BadRequestException('User by email already exists');
|
||||
}
|
||||
|
||||
const newToken = this.token.generateString(64);
|
||||
await this.cache.set(
|
||||
`register-${newToken}`,
|
||||
email,
|
||||
7 * 24 * 60 * 60 * 1000, // 7 days
|
||||
);
|
||||
|
||||
try {
|
||||
const content = InvitationEmail(
|
||||
`${this.config.get<string>('app.base_url')}/register?token=${newToken}`,
|
||||
);
|
||||
|
||||
await this.email.sendEmailTemplate(
|
||||
email,
|
||||
'You have been invited to create an account on Icy Network',
|
||||
content,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Issuing a new registration token for ${email} was successful`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Issuing a new registration token for ${email} failed: ${error}`,
|
||||
);
|
||||
await this.cache.del(`register-${newToken}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async checkRegistrationToken(token: string) {
|
||||
return await this.cache.get<string>(`register-${token}`);
|
||||
}
|
||||
|
||||
public async invalidateRegistrationToken(token: string) {
|
||||
return await this.cache.del(`register-${token}`);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import {
|
||||
Get,
|
||||
Post,
|
||||
Query,
|
||||
Render,
|
||||
Req,
|
||||
Res,
|
||||
UnauthorizedException,
|
||||
@ -31,11 +30,40 @@ export class RegisterController {
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Render('register')
|
||||
public registerView(@Req() req: Request): Record<string, unknown> {
|
||||
return this.formUtil.populateTemplate(req, {
|
||||
registrationAuthorized: this.config.get<boolean>('app.registrations'),
|
||||
});
|
||||
public async registerView(
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
@Query('token') registrationToken: string,
|
||||
@Query('redirectTo') redirectTo: string,
|
||||
) {
|
||||
let registrationAuthorized = this.config.get<boolean>('app.registrations');
|
||||
if (registrationToken) {
|
||||
const registrationEmail =
|
||||
await this.userService.checkRegistrationToken(registrationToken);
|
||||
|
||||
if (!registrationEmail) {
|
||||
req.flash('message', {
|
||||
error: true,
|
||||
text: `This registration token is invalid or expired.`,
|
||||
});
|
||||
|
||||
return res.redirect(
|
||||
'/login' + (redirectTo ? '?redirectTo=' + redirectTo : ''),
|
||||
);
|
||||
}
|
||||
|
||||
req.flash('form', { email: registrationEmail });
|
||||
|
||||
// bypass limitations
|
||||
registrationAuthorized = true;
|
||||
}
|
||||
|
||||
res.render(
|
||||
'register',
|
||||
this.formUtil.populateTemplate(req, {
|
||||
registrationAuthorized,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ -45,12 +73,34 @@ export class RegisterController {
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
@Body() body: RegisterDto,
|
||||
@Query('token') registrationToken: string,
|
||||
@Query('redirectTo') redirectTo?: string,
|
||||
) {
|
||||
const { username, display_name, email, password, password_repeat } =
|
||||
this.formUtil.trimmed(body, ['username', 'display_name', 'email']);
|
||||
|
||||
if (!this.config.get<boolean>('app.registrations')) {
|
||||
let registrationAuthorized = this.config.get<boolean>('app.registrations');
|
||||
let tokenEmail: string | undefined;
|
||||
|
||||
if (registrationToken) {
|
||||
tokenEmail =
|
||||
await this.userService.checkRegistrationToken(registrationToken);
|
||||
|
||||
if (!tokenEmail) {
|
||||
req.flash('message', {
|
||||
error: true,
|
||||
text: `This registration token is invalid or expired.`,
|
||||
});
|
||||
return res.redirect(
|
||||
'/login' + (redirectTo ? '?redirectTo=' + redirectTo : ''),
|
||||
);
|
||||
}
|
||||
|
||||
// bypass limitations
|
||||
registrationAuthorized = true;
|
||||
}
|
||||
|
||||
if (!registrationAuthorized) {
|
||||
throw new UnauthorizedException(
|
||||
'Registrations are disabled by administrator.',
|
||||
);
|
||||
@ -94,12 +144,23 @@ export class RegisterController {
|
||||
throw new Error('The passwords do not match!');
|
||||
}
|
||||
|
||||
const user = await this.userService.userRegistration(body, redirectTo);
|
||||
const sendActivationEmail = tokenEmail ? tokenEmail !== email : true;
|
||||
const user = await this.userService.userRegistration(
|
||||
body,
|
||||
redirectTo,
|
||||
!sendActivationEmail,
|
||||
);
|
||||
await this.audit.auditRequest(req, AuditAction.REGISTRATION, null, user);
|
||||
|
||||
if (tokenEmail) {
|
||||
await this.userService.invalidateRegistrationToken(registrationToken);
|
||||
}
|
||||
|
||||
req.flash('message', {
|
||||
error: false,
|
||||
text: `An activation email has been sent to ${email}!`,
|
||||
text: sendActivationEmail
|
||||
? `An activation email has been sent to ${email}!`
|
||||
: `Welcome, we have been expecting you! You may now log in.`,
|
||||
});
|
||||
|
||||
res.redirect('/login' + (redirectTo ? '?redirectTo=' + redirectTo : ''));
|
||||
|
Reference in New Issue
Block a user