Some progress
This commit is contained in:
parent
3f399320b6
commit
a21a2257db
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"eslint.validate": ["typescript", "typescriptreact", "html"],
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,9 @@ dotenv.config();
|
|||||||
const CONFIG_ENV = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';
|
const CONFIG_ENV = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';
|
||||||
const CONFIG_FILENAME = process.env.CONFIG || `config.${CONFIG_ENV}.toml`;
|
const CONFIG_FILENAME = process.env.CONFIG || `config.${CONFIG_ENV}.toml`;
|
||||||
const CONFIG_PATH = join(process.cwd(), CONFIG_FILENAME);
|
const CONFIG_PATH = join(process.cwd(), CONFIG_FILENAME);
|
||||||
|
|
||||||
|
// toml.parse returns an object that doesn't have the correct prototype,
|
||||||
|
// thus this JSON workaround is used.
|
||||||
const config = JSON.parse(
|
const config = JSON.parse(
|
||||||
JSON.stringify(toml.parse(readFileSync(CONFIG_PATH, { encoding: 'utf-8' }))),
|
JSON.stringify(toml.parse(readFileSync(CONFIG_PATH, { encoding: 'utf-8' }))),
|
||||||
);
|
);
|
||||||
|
19
package-lock.json
generated
19
package-lock.json
generated
@ -49,6 +49,7 @@
|
|||||||
"@nestjs/testing": "^8.0.0",
|
"@nestjs/testing": "^8.0.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/connect-redis": "^0.0.18",
|
"@types/connect-redis": "^0.0.18",
|
||||||
|
"@types/cookie-parser": "^1.4.3",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/express-session": "^1.17.4",
|
"@types/express-session": "^1.17.4",
|
||||||
"@types/jest": "27.4.1",
|
"@types/jest": "27.4.1",
|
||||||
@ -3146,6 +3147,15 @@
|
|||||||
"@types/redis": "^2.8.0"
|
"@types/redis": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/cookie-parser": {
|
||||||
|
"version": "1.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz",
|
||||||
|
"integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/cookiejar": {
|
"node_modules/@types/cookiejar": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
|
||||||
@ -14757,6 +14767,15 @@
|
|||||||
"@types/redis": "^2.8.0"
|
"@types/redis": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/cookie-parser": {
|
||||||
|
"version": "1.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz",
|
||||||
|
"integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/express": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/cookiejar": {
|
"@types/cookiejar": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
|
||||||
|
@ -63,6 +63,7 @@
|
|||||||
"@nestjs/testing": "^8.0.0",
|
"@nestjs/testing": "^8.0.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/connect-redis": "^0.0.18",
|
"@types/connect-redis": "^0.0.18",
|
||||||
|
"@types/cookie-parser": "^1.4.3",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/express-session": "^1.17.4",
|
"@types/express-session": "^1.17.4",
|
||||||
"@types/jest": "27.4.1",
|
"@types/jest": "27.4.1",
|
||||||
|
@ -17,6 +17,7 @@ import { OAuth2Module } from './modules/features/oauth2/oauth2.module';
|
|||||||
import { RegisterModule } from './modules/features/register/register.module';
|
import { RegisterModule } from './modules/features/register/register.module';
|
||||||
import { SettingsModule } from './modules/features/settings/settings.module';
|
import { SettingsModule } from './modules/features/settings/settings.module';
|
||||||
import { TwoFactorModule } from './modules/features/two-factor/two-factor.module';
|
import { TwoFactorModule } from './modules/features/two-factor/two-factor.module';
|
||||||
|
import { JWTModule } from './modules/jwt/jwt.module';
|
||||||
import { DatabaseModule } from './modules/objects/database/database.module';
|
import { DatabaseModule } from './modules/objects/database/database.module';
|
||||||
import { EmailModule } from './modules/objects/email/email.module';
|
import { EmailModule } from './modules/objects/email/email.module';
|
||||||
import { OAuth2ClientModule } from './modules/objects/oauth2-client/oauth2-client.module';
|
import { OAuth2ClientModule } from './modules/objects/oauth2-client/oauth2-client.module';
|
||||||
@ -47,6 +48,7 @@ import { UtilityModule } from './modules/utility/utility.module';
|
|||||||
LoginModule,
|
LoginModule,
|
||||||
RegisterModule,
|
RegisterModule,
|
||||||
OAuth2Module,
|
OAuth2Module,
|
||||||
|
JWTModule,
|
||||||
TwoFactorModule,
|
TwoFactorModule,
|
||||||
SettingsModule,
|
SettingsModule,
|
||||||
PrivilegeModule,
|
PrivilegeModule,
|
||||||
|
@ -4,6 +4,7 @@ import * as dotenv from 'dotenv';
|
|||||||
import * as session from 'express-session';
|
import * as session from 'express-session';
|
||||||
import * as connectRedis from 'connect-redis';
|
import * as connectRedis from 'connect-redis';
|
||||||
import * as redis from 'redis';
|
import * as redis from 'redis';
|
||||||
|
import * as cookieParser from 'cookie-parser';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ async function bootstrap() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// app.use(express.urlencoded());
|
// app.use(express.urlencoded());
|
||||||
// app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
||||||
// Production servers have to be behind a proxy.
|
// Production servers have to be behind a proxy.
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
@ -2,17 +2,24 @@ import { Injectable, NestMiddleware } from '@nestjs/common';
|
|||||||
import { NextFunction, Request, Response } from 'express';
|
import { NextFunction, Request, Response } from 'express';
|
||||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||||
|
|
||||||
|
const DEV = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CSRFMiddleware implements NestMiddleware {
|
export class CSRFMiddleware implements NestMiddleware {
|
||||||
constructor(private readonly tokenService: TokenService) {}
|
constructor(private readonly tokenService: TokenService) {}
|
||||||
|
|
||||||
use(req: Request, res: Response, next: NextFunction) {
|
use(req: Request, res: Response, next: NextFunction) {
|
||||||
// TODO: do not store in session, keep the amount of pointless sessions down
|
let secretToken = req.cookies.XSRF;
|
||||||
if (!req.session.csrf) {
|
if (!secretToken) {
|
||||||
req.session.csrf = this.tokenService.csrf.secretSync();
|
secretToken = this.tokenService.csrf.secretSync();
|
||||||
|
res.cookie('XSRF', secretToken, {
|
||||||
|
maxAge: 60 * 60 * 1000,
|
||||||
|
secure: !DEV,
|
||||||
|
sameSite: 'strict',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
req.csrfToken = () => this.tokenService.csrf.create(req.session.csrf);
|
req.csrfToken = () => this.tokenService.csrf.create(secretToken);
|
||||||
|
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ export const configProviders = [
|
|||||||
const file = await readFile(path, { encoding: 'utf-8' });
|
const file = await readFile(path, { encoding: 'utf-8' });
|
||||||
return {
|
return {
|
||||||
...def,
|
...def,
|
||||||
...toml.parse(file),
|
...JSON.parse(JSON.stringify(toml.parse(file))),
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('Failed to load configuration:', e.message);
|
console.error('Failed to load configuration:', e.message);
|
||||||
|
@ -53,7 +53,7 @@ export class LoginController {
|
|||||||
@Body() body: { username: string; password: string },
|
@Body() body: { username: string; password: string },
|
||||||
@Query('redirectTo') redirectTo?: string,
|
@Query('redirectTo') redirectTo?: string,
|
||||||
) {
|
) {
|
||||||
const { username, password } = body;
|
const { username, password } = this.formUtil.trimmed(body, ['username']);
|
||||||
const user = await this.userService.getByUsername(username);
|
const user = await this.userService.getByUsername(username);
|
||||||
|
|
||||||
// User exists and password matches
|
// User exists and password matches
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { OAuth2AdapterModel, OAuth2Provider } from '@icynet/oauth2-provider';
|
import { OAuth2AdapterModel, OAuth2Provider } from '@icynet/oauth2-provider';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigurationService } from 'src/modules/config/config.service';
|
||||||
import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service';
|
import { OAuth2ClientService } from 'src/modules/objects/oauth2-client/oauth2-client.service';
|
||||||
import { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service';
|
import { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service';
|
||||||
import { UserService } from 'src/modules/objects/user/user.service';
|
import { UserService } from 'src/modules/objects/user/user.service';
|
||||||
@ -55,6 +56,7 @@ export class OAuth2Service {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public token: TokenService,
|
public token: TokenService,
|
||||||
|
public config: ConfigurationService,
|
||||||
public userService: UserService,
|
public userService: UserService,
|
||||||
public clientService: OAuth2ClientService,
|
public clientService: OAuth2ClientService,
|
||||||
public tokenService: OAuth2TokenService,
|
public tokenService: OAuth2TokenService,
|
||||||
|
@ -35,7 +35,8 @@ export class RegisterController {
|
|||||||
@Body() body: RegisterDto,
|
@Body() body: RegisterDto,
|
||||||
@Query('redirectTo') redirectTo?: string,
|
@Query('redirectTo') redirectTo?: string,
|
||||||
) {
|
) {
|
||||||
const { username, display_name, email, password, password_repeat } = body;
|
const { username, display_name, email, password, password_repeat } =
|
||||||
|
this.formUtil.trimmed(body, ['username', 'display_name', 'email']);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
|
@ -56,7 +56,7 @@ export class SettingsController {
|
|||||||
@Body() body: { display_name?: string },
|
@Body() body: { display_name?: string },
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { display_name } = body;
|
const { display_name } = this._form.trimmed(body, ['display_name']);
|
||||||
if (!display_name) {
|
if (!display_name) {
|
||||||
throw new Error('Display name is required.');
|
throw new Error('Display name is required.');
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,11 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { ConfigurationService } from '../config/config.service';
|
import { ConfigurationService } from '../config/config.service';
|
||||||
import * as jwt from 'jsonwebtoken';
|
import * as jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate JWTs using the following commands:
|
||||||
|
* Private: ssh-keygen -t rsa -b 4096 -m PEM -f jwt.private.pem
|
||||||
|
* Public: openssl rsa -in jwt.private.pem -pubout -outform PEM -out jwt.public.pem
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JWTService {
|
export class JWTService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -15,7 +15,7 @@ Change your password: ${url}
|
|||||||
|
|
||||||
If you did not request a password change on Icy Network, you can safely ignore this email.
|
If you did not request a password change on Icy Network, you can safely ignore this email.
|
||||||
`,
|
`,
|
||||||
html: `
|
html: /* html */ `
|
||||||
<h1>Icy Network</h1>
|
<h1>Icy Network</h1>
|
||||||
|
|
||||||
<p><strong>Hello, ${username}! You have requested a password reset on Icy Network.</strong></p>
|
<p><strong>Hello, ${username}! You have requested a password reset on Icy Network.</strong></p>
|
||||||
|
@ -13,7 +13,7 @@ In order to proceed with logging in, please click on the following link to activ
|
|||||||
|
|
||||||
Activate your account: ${url}
|
Activate your account: ${url}
|
||||||
`,
|
`,
|
||||||
html: `
|
html: /* html */ `
|
||||||
<h1>Icy Network</h1>
|
<h1>Icy Network</h1>
|
||||||
|
|
||||||
<p><strong>Welcome to Icy Network, ${username}!</strong></p>
|
<p><strong>Welcome to Icy Network, ${username}!</strong></p>
|
||||||
|
@ -25,14 +25,26 @@ export class UserService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getById(id: number, relations?: string[]): Promise<User> {
|
public async getById(id: number, relations?: string[]): Promise<User> {
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return this.userRepository.findOne({ id }, { relations });
|
return this.userRepository.findOne({ id }, { relations });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getByUUID(uuid: string, relations?: string[]): Promise<User> {
|
public async getByUUID(uuid: string, relations?: string[]): Promise<User> {
|
||||||
|
if (!uuid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return this.userRepository.findOne({ uuid }, { relations });
|
return this.userRepository.findOne({ uuid }, { relations });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getByEmail(email: string, relations?: string[]): Promise<User> {
|
public async getByEmail(email: string, relations?: string[]): Promise<User> {
|
||||||
|
if (!email) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return this.userRepository.findOne({ email }, { relations });
|
return this.userRepository.findOne({ email }, { relations });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,6 +52,10 @@ export class UserService {
|
|||||||
username: string,
|
username: string,
|
||||||
relations?: string[],
|
relations?: string[],
|
||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
|
if (!username) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return this.userRepository.findOne({ username }, { relations });
|
return this.userRepository.findOne({ username }, { relations });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,10 +15,16 @@ export class FormUtilityService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public trimmed<T>(entry: T, fields: string[]): T {
|
||||||
|
return fields.reduce<T>(
|
||||||
|
(object, key) => ({ ...object, [key]: object[key]?.trim() }),
|
||||||
|
entry,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Include CSRF token, messages and prefilled form values for a template with a form
|
* Include CSRF token, messages and prefilled form values for a template with a form
|
||||||
* @param req Express request
|
* @param req Express request
|
||||||
* @param session Express session
|
|
||||||
* @param additional Additional data to pass
|
* @param additional Additional data to pass
|
||||||
* @returns Template locals
|
* @returns Template locals
|
||||||
*/
|
*/
|
||||||
|
@ -18,7 +18,7 @@ export class TokenService {
|
|||||||
constructor(private config: ConfigurationService) {}
|
constructor(private config: ConfigurationService) {}
|
||||||
|
|
||||||
public verifyCSRF(req: Request, token?: string): boolean {
|
public verifyCSRF(req: Request, token?: string): boolean {
|
||||||
return this.csrf.verify(req.session.csrf, token || req.body._csrf);
|
return this.csrf.verify(req.cookies.XSRF, token || req.body._csrf);
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateString(length: number): string {
|
public generateString(length: number): string {
|
||||||
|
@ -17,7 +17,7 @@ block settings
|
|||||||
h2 Change Password
|
h2 Change Password
|
||||||
form(method="post", action="/account/security/password", autocomplete="off")
|
form(method="post", action="/account/security/password", autocomplete="off")
|
||||||
div.form-container
|
div.form-container
|
||||||
input#csrf(type="hidden", name="_csrf", value=csrf)
|
input#csrfa(type="hidden", name="_csrf", value=csrf)
|
||||||
label.form-label(for="password") Current Password
|
label.form-label(for="password") Current Password
|
||||||
input.form-control#password(type="password", name="password")
|
input.form-control#password(type="password", name="password")
|
||||||
label.form-label(for="new_password") New Password
|
label.form-label(for="new_password") New Password
|
||||||
@ -29,7 +29,7 @@ block settings
|
|||||||
h2 Change Email Address
|
h2 Change Email Address
|
||||||
form(method="post", action="/account/security/email", autocomplete="off")
|
form(method="post", action="/account/security/email", autocomplete="off")
|
||||||
div.form-container
|
div.form-container
|
||||||
input#csrf(type="hidden", name="_csrf", value=csrf)
|
input#csrfb(type="hidden", name="_csrf", value=csrf)
|
||||||
label.form-label(for="current_email") Current Email Address
|
label.form-label(for="current_email") Current Email Address
|
||||||
input.form-control#current_email(type="email", name="current_email")
|
input.form-control#current_email(type="email", name="current_email")
|
||||||
small.form-hint Hint: #{emailHint}
|
small.form-hint Hint: #{emailHint}
|
||||||
|
Reference in New Issue
Block a user