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_FILENAME = process.env.CONFIG || `config.${CONFIG_ENV}.toml`;
|
||||
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(
|
||||
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",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/connect-redis": "^0.0.18",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-session": "^1.17.4",
|
||||
"@types/jest": "27.4.1",
|
||||
@ -3146,6 +3147,15 @@
|
||||
"@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": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
|
||||
@ -14757,6 +14767,15 @@
|
||||
"@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": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
|
||||
|
@ -63,6 +63,7 @@
|
||||
"@nestjs/testing": "^8.0.0",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/connect-redis": "^0.0.18",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-session": "^1.17.4",
|
||||
"@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 { SettingsModule } from './modules/features/settings/settings.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 { EmailModule } from './modules/objects/email/email.module';
|
||||
import { OAuth2ClientModule } from './modules/objects/oauth2-client/oauth2-client.module';
|
||||
@ -47,6 +48,7 @@ import { UtilityModule } from './modules/utility/utility.module';
|
||||
LoginModule,
|
||||
RegisterModule,
|
||||
OAuth2Module,
|
||||
JWTModule,
|
||||
TwoFactorModule,
|
||||
SettingsModule,
|
||||
PrivilegeModule,
|
||||
|
@ -4,6 +4,7 @@ import * as dotenv from 'dotenv';
|
||||
import * as session from 'express-session';
|
||||
import * as connectRedis from 'connect-redis';
|
||||
import * as redis from 'redis';
|
||||
import * as cookieParser from 'cookie-parser';
|
||||
import { join } from 'path';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
|
||||
@ -19,7 +20,7 @@ async function bootstrap() {
|
||||
});
|
||||
|
||||
// app.use(express.urlencoded());
|
||||
// app.use(cookieParser());
|
||||
app.use(cookieParser());
|
||||
|
||||
// Production servers have to be behind a proxy.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
|
@ -2,17 +2,24 @@ import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { TokenService } from 'src/modules/utility/services/token.service';
|
||||
|
||||
const DEV = process.env.NODE_ENV !== 'production';
|
||||
|
||||
@Injectable()
|
||||
export class CSRFMiddleware implements NestMiddleware {
|
||||
constructor(private readonly tokenService: TokenService) {}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
// TODO: do not store in session, keep the amount of pointless sessions down
|
||||
if (!req.session.csrf) {
|
||||
req.session.csrf = this.tokenService.csrf.secretSync();
|
||||
let secretToken = req.cookies.XSRF;
|
||||
if (!secretToken) {
|
||||
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();
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ export const configProviders = [
|
||||
const file = await readFile(path, { encoding: 'utf-8' });
|
||||
return {
|
||||
...def,
|
||||
...toml.parse(file),
|
||||
...JSON.parse(JSON.stringify(toml.parse(file))),
|
||||
};
|
||||
} catch (e: any) {
|
||||
console.error('Failed to load configuration:', e.message);
|
||||
|
@ -53,7 +53,7 @@ export class LoginController {
|
||||
@Body() body: { username: string; password: string },
|
||||
@Query('redirectTo') redirectTo?: string,
|
||||
) {
|
||||
const { username, password } = body;
|
||||
const { username, password } = this.formUtil.trimmed(body, ['username']);
|
||||
const user = await this.userService.getByUsername(username);
|
||||
|
||||
// User exists and password matches
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { OAuth2AdapterModel, OAuth2Provider } from '@icynet/oauth2-provider';
|
||||
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 { OAuth2TokenService } from 'src/modules/objects/oauth2-token/oauth2-token.service';
|
||||
import { UserService } from 'src/modules/objects/user/user.service';
|
||||
@ -55,6 +56,7 @@ export class OAuth2Service {
|
||||
|
||||
constructor(
|
||||
public token: TokenService,
|
||||
public config: ConfigurationService,
|
||||
public userService: UserService,
|
||||
public clientService: OAuth2ClientService,
|
||||
public tokenService: OAuth2TokenService,
|
||||
|
@ -35,7 +35,8 @@ export class RegisterController {
|
||||
@Body() body: RegisterDto,
|
||||
@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 {
|
||||
if (
|
||||
|
@ -56,7 +56,7 @@ export class SettingsController {
|
||||
@Body() body: { display_name?: string },
|
||||
) {
|
||||
try {
|
||||
const { display_name } = body;
|
||||
const { display_name } = this._form.trimmed(body, ['display_name']);
|
||||
if (!display_name) {
|
||||
throw new Error('Display name is required.');
|
||||
}
|
||||
|
@ -2,6 +2,11 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ConfigurationService } from '../config/config.service';
|
||||
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()
|
||||
export class JWTService {
|
||||
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.
|
||||
`,
|
||||
html: `
|
||||
html: /* html */ `
|
||||
<h1>Icy Network</h1>
|
||||
|
||||
<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}
|
||||
`,
|
||||
html: `
|
||||
html: /* html */ `
|
||||
<h1>Icy Network</h1>
|
||||
|
||||
<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> {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.userRepository.findOne({ id }, { relations });
|
||||
}
|
||||
|
||||
public async getByUUID(uuid: string, relations?: string[]): Promise<User> {
|
||||
if (!uuid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.userRepository.findOne({ uuid }, { relations });
|
||||
}
|
||||
|
||||
public async getByEmail(email: string, relations?: string[]): Promise<User> {
|
||||
if (!email) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.userRepository.findOne({ email }, { relations });
|
||||
}
|
||||
|
||||
@ -40,6 +52,10 @@ export class UserService {
|
||||
username: string,
|
||||
relations?: string[],
|
||||
): Promise<User> {
|
||||
if (!username) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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
|
||||
* @param req Express request
|
||||
* @param session Express session
|
||||
* @param additional Additional data to pass
|
||||
* @returns Template locals
|
||||
*/
|
||||
|
@ -18,7 +18,7 @@ export class TokenService {
|
||||
constructor(private config: ConfigurationService) {}
|
||||
|
||||
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 {
|
||||
|
@ -17,7 +17,7 @@ block settings
|
||||
h2 Change Password
|
||||
form(method="post", action="/account/security/password", autocomplete="off")
|
||||
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
|
||||
input.form-control#password(type="password", name="password")
|
||||
label.form-label(for="new_password") New Password
|
||||
@ -29,7 +29,7 @@ block settings
|
||||
h2 Change Email Address
|
||||
form(method="post", action="/account/security/email", autocomplete="off")
|
||||
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
|
||||
input.form-control#current_email(type="email", name="current_email")
|
||||
small.form-hint Hint: #{emailHint}
|
||||
|
Reference in New Issue
Block a user