diff --git a/src/main.ts b/src/main.ts index 250be00..324eb13 100644 --- a/src/main.ts +++ b/src/main.ts @@ -44,6 +44,7 @@ async function bootstrap() { } app.use( + /\/((?!api).)*/, session({ secret: process.env.SESSION_SECRET, resave: true, diff --git a/src/modules/api/admin/oauth2-admin.controller.ts b/src/modules/api/admin/oauth2-admin.controller.ts index 19f4421..ea018d9 100644 --- a/src/modules/api/admin/oauth2-admin.controller.ts +++ b/src/modules/api/admin/oauth2-admin.controller.ts @@ -48,6 +48,7 @@ const SET_CLIENT_FIELDS = [ 'grants', 'activated', 'verified', + 'urls', ]; const URL_TYPES = ['redirect_uri', 'terms', 'privacy', 'website']; @@ -167,6 +168,10 @@ export class OAuth2AdminController { @CurrentUser() user: User, ) { const client = await this._oaClient.getById(parseInt(id, 10), []); + const reducedPermissions = !this._service.userHasPrivilege( + user, + 'admin:oauth2', + ); if (!client) { throw new NotFoundException('Client not found'); } @@ -178,9 +183,11 @@ export class OAuth2AdminController { } const allowedFieldsOnly = this._form.pluckObject(setter, SET_CLIENT_FIELDS); + const urls = allowedFieldsOnly.urls?.slice(); + delete allowedFieldsOnly.urls; // Non-admin cannot change the activated or verified status - if (!this._service.userHasPrivilege(user, 'admin:oauth2')) { + if (reducedPermissions) { delete allowedFieldsOnly.activated; delete allowedFieldsOnly.verified; } @@ -189,9 +196,33 @@ export class OAuth2AdminController { return this._oaClient.stripClientInfo(client); } + const splitGrants = allowedFieldsOnly.grants.split(' '); + const splitScopes = allowedFieldsOnly.scope.split(' '); + let availableGrantTypes = this._oaClient.availableGrantTypes; + let availableScopes = this._oaClient.availableScopes; + + if (reducedPermissions) { + availableGrantTypes = + this._service.removeUnprivileged(availableGrantTypes); + availableScopes = this._service.removeUnprivileged(availableScopes); + allowedFieldsOnly.activated = true; + } + + if (!splitGrants.every((grant) => availableGrantTypes.includes(grant))) { + throw new BadRequestException('Bad grant types'); + } + + if (!splitScopes.every((scope) => availableScopes.includes(scope))) { + throw new BadRequestException('Bad scopes'); + } + Object.assign(client, allowedFieldsOnly); await this._oaClient.updateClient(client); + if (urls?.length) { + await this._oaClient.upsertURLs(client, urls); + } + return this._oaClient.stripClientInfo(client); } @@ -484,6 +515,9 @@ export class OAuth2AdminController { throw new BadRequestException('Bad scopes'); } + const urls = setter.urls?.slice(); + delete allowedFieldsOnly.urls; + const client = new OAuth2Client(); Object.assign(client, allowedFieldsOnly); client.client_id = this._token.createUUID(); @@ -491,6 +525,10 @@ export class OAuth2AdminController { client.owner = user; await this._oaClient.updateClient(client); + if (urls?.length) { + await this._oaClient.upsertURLs(client, urls); + } + return this._oaClient.stripClientInfo(client); } } diff --git a/src/modules/api/admin/user-admin.controller.ts b/src/modules/api/admin/user-admin.controller.ts index 59be8d2..235085a 100644 --- a/src/modules/api/admin/user-admin.controller.ts +++ b/src/modules/api/admin/user-admin.controller.ts @@ -6,6 +6,7 @@ import { Get, NotFoundException, Param, + Patch, Post, Put, Query, @@ -18,6 +19,7 @@ import { OAuth2Guard } from 'src/guards/oauth2.guard'; import { PrivilegesGuard } from 'src/guards/privileges.guard'; import { ScopesGuard } from 'src/guards/scopes.guard'; import { PrivilegeService } from 'src/modules/objects/privilege/privilege.service'; +import { User } from 'src/modules/objects/user/user.entity'; import { UserService } from 'src/modules/objects/user/user.service'; import { FormUtilityService } from 'src/modules/utility/services/form-utility.service'; import { PaginationService } from 'src/modules/utility/services/paginate.service'; @@ -80,6 +82,32 @@ export class UserAdminController { return this._form.stripObject(user, ['password']); } + @Patch(':id') + @Scopes('management') + @Privileges('admin', 'admin:user') + async updateUser(@Param('id') id: string, @Body() body: Partial) { + const user = await this._user.getById(parseInt(id, 10)); + if (!user) { + throw new NotFoundException('User not found'); + } + + const allowedFieldsOnly = this._form.pluckObject(body, [ + 'display_name', + 'username', + 'email', + 'activated', + ]); + + if (!Object.keys(allowedFieldsOnly).length) { + throw new BadRequestException('No data was updated.'); + } + + Object.assign(user, allowedFieldsOnly); + await this._user.updateUser(user); + + return user; + } + /** * Delete a user avatar from the server * @param id User ID @@ -162,7 +190,7 @@ export class UserAdminController { * @param id User ID * @returns Success or error */ - @Post(':id/activate') + @Post(':id/activation') @Scopes('management') @Privileges('admin', 'admin:user') async activateUserEmail(@Param('id') id: string) { diff --git a/src/modules/oauth2/oauth2.service.ts b/src/modules/oauth2/oauth2.service.ts index cee2c84..d7e89bc 100644 --- a/src/modules/oauth2/oauth2.service.ts +++ b/src/modules/oauth2/oauth2.service.ts @@ -69,9 +69,9 @@ export class OAuth2Service { public clientService: OAuth2ClientService, public tokenService: OAuth2TokenService, ) { - if (process.env.NODE_ENV === 'development') { - this.oauth.logger.setLogLevel('debug'); - } + // if (process.env.NODE_ENV === 'development') { + // this.oauth.logger.setLogLevel('debug'); + // } } public splitScope(scope: string | string[]): string[] { diff --git a/src/modules/objects/oauth2-client/oauth2-client.service.ts b/src/modules/objects/oauth2-client/oauth2-client.service.ts index d324946..ea6e532 100644 --- a/src/modules/objects/oauth2-client/oauth2-client.service.ts +++ b/src/modules/objects/oauth2-client/oauth2-client.service.ts @@ -168,6 +168,7 @@ export class OAuth2ClientService { : undefined, skip: offset, take: limit, + order: { id: 'asc' }, relations, }); } @@ -197,6 +198,7 @@ export class OAuth2ClientService { ): Promise { return this.clientRepository.find({ where: { owner: { id: owner.id } }, + order: { id: 'asc' }, relations, }); } @@ -234,6 +236,37 @@ export class OAuth2ClientService { })); } + public async upsertURLs( + client: OAuth2Client, + urls: OAuth2ClientURL[], + ): Promise { + const existingURLs = await this.getClientURLs(client.client_id); + const removed = []; + + for (const existing of existingURLs) { + const alsoProvided = urls.find(({ id }) => id === existing.id); + if (alsoProvided && !!alsoProvided.url) { + Object.assign(existing, alsoProvided); + await this.updateClientURL(existing); + } else { + await this.deleteClientURL(existing); + removed.push(existing); + } + } + + for (const newUrl of urls.filter((url) => !url.id)) { + const newUrlObject = this.reobjectifyURL(newUrl, client); + await this.updateClientURL(newUrlObject); + existingURLs.push(newUrlObject); + } + + client.urls = existingURLs + .filter(({ id }) => !removed.some((removed) => removed.id === id)) + .map((itm) => ({ ...itm, client: undefined })); + + return client; + } + public async updateClient(client: OAuth2Client): Promise { await this.clientRepository.save(client); return client; @@ -289,4 +322,14 @@ export class OAuth2ClientService { : null, } as Partial; } + + private reobjectifyURL( + input: Partial, + client: OAuth2Client, + ): OAuth2ClientURL { + const reObjectifyURL = new OAuth2ClientURL(); + Object.assign(reObjectifyURL, input); + reObjectifyURL.client = client; + return reObjectifyURL; + } } diff --git a/src/modules/static-front-end/login/login.module.ts b/src/modules/static-front-end/login/login.module.ts index cb99322..bf55ddb 100644 --- a/src/modules/static-front-end/login/login.module.ts +++ b/src/modules/static-front-end/login/login.module.ts @@ -4,7 +4,9 @@ import { NestModule, RequestMethod, } from '@nestjs/common'; +import { CSRFMiddleware } from 'src/middleware/csrf.middleware'; import { FlashMiddleware } from 'src/middleware/flash.middleware'; +import { UserMiddleware } from 'src/middleware/user.middleware'; import { ValidateCSRFMiddleware } from 'src/middleware/validate-csrf.middleware'; import { UserTokenModule } from 'src/modules/objects/user-token/user-token.module'; import { UserModule } from 'src/modules/objects/user/user.module'; @@ -16,6 +18,7 @@ import { LoginController } from './login.controller'; }) export class LoginModule implements NestModule { configure(consumer: MiddlewareConsumer) { + consumer.apply(CSRFMiddleware, UserMiddleware).forRoutes(LoginController); consumer .apply(ValidateCSRFMiddleware) .forRoutes({ path: 'login*', method: RequestMethod.POST }); diff --git a/src/modules/static-front-end/oauth2-router/oauth2-router.module.ts b/src/modules/static-front-end/oauth2-router/oauth2-router.module.ts index 5766c2e..326596e 100644 --- a/src/modules/static-front-end/oauth2-router/oauth2-router.module.ts +++ b/src/modules/static-front-end/oauth2-router/oauth2-router.module.ts @@ -1,5 +1,7 @@ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { AuthMiddleware } from 'src/middleware/auth.middleware'; +import { CSRFMiddleware } from 'src/middleware/csrf.middleware'; +import { UserMiddleware } from 'src/middleware/user.middleware'; import { ValidateCSRFMiddleware } from 'src/middleware/validate-csrf.middleware'; import { OAuth2Module } from 'src/modules/oauth2/oauth2.module'; import { OAuth2Service } from 'src/modules/oauth2/oauth2.service'; @@ -14,6 +16,7 @@ export class OAuth2RouterModule implements NestModule { constructor(private _service: OAuth2Service) {} configure(consumer: MiddlewareConsumer) { + consumer.apply(CSRFMiddleware, UserMiddleware).forRoutes(OAuth2Controller); consumer.apply(this._service.oauth.express()).forRoutes('oauth2/*'); consumer .apply(AuthMiddleware, ValidateCSRFMiddleware) diff --git a/src/modules/static-front-end/register/register.module.ts b/src/modules/static-front-end/register/register.module.ts index dcfb205..3f5f553 100644 --- a/src/modules/static-front-end/register/register.module.ts +++ b/src/modules/static-front-end/register/register.module.ts @@ -4,7 +4,9 @@ import { NestModule, RequestMethod, } from '@nestjs/common'; +import { CSRFMiddleware } from 'src/middleware/csrf.middleware'; import { FlashMiddleware } from 'src/middleware/flash.middleware'; +import { UserMiddleware } from 'src/middleware/user.middleware'; import { ValidateCSRFMiddleware } from 'src/middleware/validate-csrf.middleware'; import { UserModule } from 'src/modules/objects/user/user.module'; import { RegisterController } from './register.controller'; @@ -15,6 +17,10 @@ import { RegisterController } from './register.controller'; }) export class RegisterModule implements NestModule { configure(consumer: MiddlewareConsumer) { + consumer + .apply(CSRFMiddleware, UserMiddleware) + .forRoutes(RegisterController); + consumer .apply(ValidateCSRFMiddleware) .forRoutes({ path: 'register*', method: RequestMethod.POST }); diff --git a/src/modules/static-front-end/settings/settings.module.ts b/src/modules/static-front-end/settings/settings.module.ts index 70b3aa7..b40a2f3 100644 --- a/src/modules/static-front-end/settings/settings.module.ts +++ b/src/modules/static-front-end/settings/settings.module.ts @@ -21,6 +21,8 @@ import { UserTokenModule } from 'src/modules/objects/user-token/user-token.modul import { OAuth2Module } from '../../oauth2/oauth2.module'; import { SettingsController } from './settings.controller'; import { SettingsService } from './settings.service'; +import { CSRFMiddleware } from 'src/middleware/csrf.middleware'; +import { UserMiddleware } from 'src/middleware/user.middleware'; @Module({ controllers: [SettingsController], @@ -69,6 +71,9 @@ import { SettingsService } from './settings.service'; }) export class SettingsModule implements NestModule { configure(consumer: MiddlewareConsumer) { + consumer + .apply(CSRFMiddleware, UserMiddleware) + .forRoutes(SettingsController); consumer .apply(ValidateCSRFMiddleware) .forRoutes( diff --git a/src/modules/static-front-end/static-front-end.module.ts b/src/modules/static-front-end/static-front-end.module.ts index 9ada73a..d668ccd 100644 --- a/src/modules/static-front-end/static-front-end.module.ts +++ b/src/modules/static-front-end/static-front-end.module.ts @@ -1,11 +1,4 @@ -import { - MiddlewareConsumer, - Module, - NestModule, - RequestMethod, -} from '@nestjs/common'; -import { CSRFMiddleware } from 'src/middleware/csrf.middleware'; -import { UserMiddleware } from 'src/middleware/user.middleware'; +import { Module } from '@nestjs/common'; import { ConfigurationModule } from '../config/config.module'; import { JWTModule } from '../jwt/jwt.module'; import { ObjectsModule } from '../objects/objects.module'; @@ -32,15 +25,4 @@ import { TwoFactorModule } from './two-factor/two-factor.module'; providers: [], exports: [], }) -export class StaticFrontEndModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - consumer - .apply(CSRFMiddleware, UserMiddleware) - .exclude( - { path: 'uploads*', method: RequestMethod.ALL }, - { path: 'public*', method: RequestMethod.ALL }, - { path: 'api*', method: RequestMethod.ALL }, - ) - .forRoutes('*'); - } -} +export class StaticFrontEndModule {} diff --git a/src/modules/static-front-end/two-factor/two-factor.module.ts b/src/modules/static-front-end/two-factor/two-factor.module.ts index a0b4843..52f0cf9 100644 --- a/src/modules/static-front-end/two-factor/two-factor.module.ts +++ b/src/modules/static-front-end/two-factor/two-factor.module.ts @@ -1,6 +1,8 @@ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { AuthMiddleware } from 'src/middleware/auth.middleware'; +import { CSRFMiddleware } from 'src/middleware/csrf.middleware'; import { FlashMiddleware } from 'src/middleware/flash.middleware'; +import { UserMiddleware } from 'src/middleware/user.middleware'; import { UserTokenModule } from 'src/modules/objects/user-token/user-token.module'; import { UserModule } from 'src/modules/objects/user/user.module'; import { TwoFactorController } from './two-factor.controller'; @@ -11,6 +13,9 @@ import { TwoFactorController } from './two-factor.controller'; }) export class TwoFactorModule implements NestModule { configure(consumer: MiddlewareConsumer) { + consumer + .apply(CSRFMiddleware, UserMiddleware) + .forRoutes(TwoFactorController); consumer.apply(AuthMiddleware).forRoutes('account/two-factor/activate'); consumer.apply(FlashMiddleware).forRoutes('account/two-factor/activate'); }