rearrange stuff

This commit is contained in:
Evert Prants 2022-08-27 11:59:26 +03:00
parent 9e68698ddc
commit bb86a25ad4
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
40 changed files with 508 additions and 195 deletions

225
package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "0.0.1", "version": "0.0.1",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@icynet/oauth2-provider": "git+ssh://git@gitlab.icynet.eu:IcyNetwork/oauth2-provider.git", "@icynet/oauth2-provider": "git+https://gitlab.icynet.eu/IcyNetwork/oauth2-provider.git",
"@nestjs/common": "^9.0.11", "@nestjs/common": "^9.0.11",
"@nestjs/core": "^9.0.11", "@nestjs/core": "^9.0.11",
"@nestjs/platform-express": "^9.0.11", "@nestjs/platform-express": "^9.0.11",
@ -34,7 +34,7 @@
"otplib": "^12.0.1", "otplib": "^12.0.1",
"pug": "^3.0.2", "pug": "^3.0.2",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"redis": "^3.1.2", "redis": "^4.3.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.5.6", "rxjs": "^7.5.6",
@ -2135,7 +2135,7 @@
}, },
"node_modules/@icynet/oauth2-provider": { "node_modules/@icynet/oauth2-provider": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "git+ssh://git@gitlab.icynet.eu:IcyNetwork/oauth2-provider.git#a440d1f4ac53ccb6989dd25221797490611e240a", "resolved": "git+https://gitlab.icynet.eu/IcyNetwork/oauth2-provider.git#a440d1f4ac53ccb6989dd25221797490611e240a",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"express": "^4.17.3", "express": "^4.17.3",
@ -3147,6 +3147,59 @@
"@otplib/plugin-thirty-two": "^12.0.1" "@otplib/plugin-thirty-two": "^12.0.1"
} }
}, },
"node_modules/@redis/bloom": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.0.2.tgz",
"integrity": "sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/client": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.3.0.tgz",
"integrity": "sha512-XCFV60nloXAefDsPnYMjHGtvbtHR8fV5Om8cQ0JYqTNbWcQo/4AryzJ2luRj4blveWazRK/j40gES8M7Cp6cfQ==",
"dependencies": {
"cluster-key-slot": "1.1.0",
"generic-pool": "3.8.2",
"yallist": "4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@redis/graph": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.0.1.tgz",
"integrity": "sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/json": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.3.tgz",
"integrity": "sha512-4X0Qv0BzD9Zlb0edkUoau5c1bInWSICqXAGrpwEltkncUwcxJIGEcVryZhLgb0p/3PkKaLIWkjhHRtLe9yiA7Q==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/search": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.0.tgz",
"integrity": "sha512-NyFZEVnxIJEybpy+YskjgOJRNsfTYqaPbK/Buv6W2kmFNaRk85JiqjJZA5QkRmWvGbyQYwoO5QfDi2wHskKrQQ==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/time-series": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.3.tgz",
"integrity": "sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@sinclair/typebox": { "node_modules/@sinclair/typebox": {
"version": "0.24.28", "version": "0.24.28",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.28.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.28.tgz",
@ -4997,6 +5050,14 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/cluster-key-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/co": { "node_modules/co": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -6713,6 +6774,14 @@
"is-property": "^1.0.2" "is-property": "^1.0.2"
} }
}, },
"node_modules/generic-pool": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz",
"integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==",
"engines": {
"node": ">= 4"
}
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -10122,53 +10191,16 @@
} }
}, },
"node_modules/redis": { "node_modules/redis": {
"version": "3.1.2", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", "resolved": "https://registry.npmjs.org/redis/-/redis-4.3.0.tgz",
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", "integrity": "sha512-RXRUor0iU1vizu4viHoUyLpe1ZO/RngZp0V9DyXBHTI+7tC7rEz6Wzn4Sv9v0tTJeqGAzdJ+q5YVbNKKQ5hX9A==",
"dependencies": { "dependencies": {
"denque": "^1.5.0", "@redis/bloom": "1.0.2",
"redis-commands": "^1.7.0", "@redis/client": "1.3.0",
"redis-errors": "^1.2.0", "@redis/graph": "1.0.1",
"redis-parser": "^3.0.0" "@redis/json": "1.0.3",
}, "@redis/search": "1.1.0",
"engines": { "@redis/time-series": "1.0.3"
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-redis"
}
},
"node_modules/redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/redis/node_modules/denque": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==",
"engines": {
"node": ">=0.10"
} }
}, },
"node_modules/reflect-metadata": { "node_modules/reflect-metadata": {
@ -13918,8 +13950,8 @@
"dev": true "dev": true
}, },
"@icynet/oauth2-provider": { "@icynet/oauth2-provider": {
"version": "git+ssh://git@gitlab.icynet.eu:IcyNetwork/oauth2-provider.git#a440d1f4ac53ccb6989dd25221797490611e240a", "version": "git+https://gitlab.icynet.eu/IcyNetwork/oauth2-provider.git#a440d1f4ac53ccb6989dd25221797490611e240a",
"from": "@icynet/oauth2-provider@git+ssh://git@gitlab.icynet.eu:IcyNetwork/oauth2-provider.git", "from": "@icynet/oauth2-provider@git+https://gitlab.icynet.eu/IcyNetwork/oauth2-provider.git",
"requires": { "requires": {
"express": "^4.17.3", "express": "^4.17.3",
"express-session": "^1.17.2" "express-session": "^1.17.2"
@ -14682,6 +14714,46 @@
"@otplib/plugin-thirty-two": "^12.0.1" "@otplib/plugin-thirty-two": "^12.0.1"
} }
}, },
"@redis/bloom": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.0.2.tgz",
"integrity": "sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw==",
"requires": {}
},
"@redis/client": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.3.0.tgz",
"integrity": "sha512-XCFV60nloXAefDsPnYMjHGtvbtHR8fV5Om8cQ0JYqTNbWcQo/4AryzJ2luRj4blveWazRK/j40gES8M7Cp6cfQ==",
"requires": {
"cluster-key-slot": "1.1.0",
"generic-pool": "3.8.2",
"yallist": "4.0.0"
}
},
"@redis/graph": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.0.1.tgz",
"integrity": "sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ==",
"requires": {}
},
"@redis/json": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.3.tgz",
"integrity": "sha512-4X0Qv0BzD9Zlb0edkUoau5c1bInWSICqXAGrpwEltkncUwcxJIGEcVryZhLgb0p/3PkKaLIWkjhHRtLe9yiA7Q==",
"requires": {}
},
"@redis/search": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.0.tgz",
"integrity": "sha512-NyFZEVnxIJEybpy+YskjgOJRNsfTYqaPbK/Buv6W2kmFNaRk85JiqjJZA5QkRmWvGbyQYwoO5QfDi2wHskKrQQ==",
"requires": {}
},
"@redis/time-series": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.3.tgz",
"integrity": "sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA==",
"requires": {}
},
"@sinclair/typebox": { "@sinclair/typebox": {
"version": "0.24.28", "version": "0.24.28",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.28.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.28.tgz",
@ -16157,6 +16229,11 @@
"shallow-clone": "^3.0.0" "shallow-clone": "^3.0.0"
} }
}, },
"cluster-key-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw=="
},
"co": { "co": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -17475,6 +17552,11 @@
"is-property": "^1.0.2" "is-property": "^1.0.2"
} }
}, },
"generic-pool": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz",
"integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg=="
},
"gensync": { "gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -20051,39 +20133,16 @@
} }
}, },
"redis": { "redis": {
"version": "3.1.2", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", "resolved": "https://registry.npmjs.org/redis/-/redis-4.3.0.tgz",
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", "integrity": "sha512-RXRUor0iU1vizu4viHoUyLpe1ZO/RngZp0V9DyXBHTI+7tC7rEz6Wzn4Sv9v0tTJeqGAzdJ+q5YVbNKKQ5hX9A==",
"requires": { "requires": {
"denque": "^1.5.0", "@redis/bloom": "1.0.2",
"redis-commands": "^1.7.0", "@redis/client": "1.3.0",
"redis-errors": "^1.2.0", "@redis/graph": "1.0.1",
"redis-parser": "^3.0.0" "@redis/json": "1.0.3",
}, "@redis/search": "1.1.0",
"dependencies": { "@redis/time-series": "1.0.3"
"denque": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw=="
}
}
},
"redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
},
"redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="
},
"redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"requires": {
"redis-errors": "^1.0.0"
} }
}, },
"reflect-metadata": { "reflect-metadata": {

View File

@ -49,7 +49,7 @@
"otplib": "^12.0.1", "otplib": "^12.0.1",
"pug": "^3.0.2", "pug": "^3.0.2",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"redis": "^3.1.2", "redis": "^4.3.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.5.6", "rxjs": "^7.5.6",

View File

@ -1,30 +1,15 @@
import { import { Module } from '@nestjs/common';
MiddlewareConsumer,
Module,
NestModule,
RequestMethod,
} from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static'; import { ServeStaticModule } from '@nestjs/serve-static';
import { ThrottlerModule } from '@nestjs/throttler'; import { ThrottlerModule } from '@nestjs/throttler';
import { join } from 'path'; import { join } from 'path';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { CSRFMiddleware } from './middleware/csrf.middleware'; import { CSRFMiddleware } from './middleware/csrf.middleware';
import { UserMiddleware } from './middleware/user.middleware'; import { ApiModule } from './modules/api/api.module';
import { ConfigurationModule } from './modules/config/config.module'; import { ConfigurationModule } from './modules/config/config.module';
import { LoginModule } from './modules/features/login/login.module';
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 { JWTModule } from './modules/jwt/jwt.module';
import { DatabaseModule } from './modules/objects/database/database.module'; import { StaticFrontEndModule } from './modules/static-front-end/static-front-end.module';
import { EmailModule } from './modules/objects/email/email.module';
import { OAuth2ClientModule } from './modules/objects/oauth2-client/oauth2-client.module';
import { OAuth2TokenModule } from './modules/objects/oauth2-token/oauth2-token.module';
import { PrivilegeModule } from './modules/objects/privilege/privilege.module';
import { UploadModule } from './modules/objects/upload/upload.module';
import { UserModule } from './modules/objects/user/user.module';
import { UtilityModule } from './modules/utility/utility.module'; import { UtilityModule } from './modules/utility/utility.module';
@Module({ @Module({
@ -39,31 +24,11 @@ import { UtilityModule } from './modules/utility/utility.module';
}), }),
ConfigurationModule, ConfigurationModule,
UtilityModule, UtilityModule,
DatabaseModule,
EmailModule,
UserModule,
UploadModule,
OAuth2ClientModule,
OAuth2TokenModule,
LoginModule,
RegisterModule,
OAuth2Module,
JWTModule, JWTModule,
TwoFactorModule, StaticFrontEndModule,
SettingsModule, ApiModule,
PrivilegeModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService, CSRFMiddleware], providers: [AppService, CSRFMiddleware],
}) })
export class AppModule implements NestModule { export class AppModule {}
configure(consumer: MiddlewareConsumer) {
consumer
.apply(CSRFMiddleware, UserMiddleware)
.exclude(
{ path: 'uploads*', method: RequestMethod.ALL },
{ path: 'public*', method: RequestMethod.ALL },
)
.forRoutes('*');
}
}

View File

@ -0,0 +1,13 @@
import { OAuth2AccessToken } from '@icynet/oauth2-provider';
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
/**
* Get the OAuth2 bearer token from the response.
* Requires the OAuth2 guard or bearer middleware!
*/
export const Bearer = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const response = ctx.switchToHttp().getResponse();
return response.locals.accessToken as OAuth2AccessToken;
},
);

View File

@ -0,0 +1,8 @@
import { SetMetadata } from '@nestjs/common';
/**
* Restrict this route to only these privileges. AND logic!
* @param privileges List of privileges for this route
*/
export const Privileges = (...privileges: string[]) =>
SetMetadata('privileges', privileges);

View File

@ -0,0 +1,14 @@
import { OAuth2AccessToken } from '@icynet/oauth2-provider';
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
/**
* Get the OAuth2 access token scope from the response.
* Requires the OAuth2 guard or bearer middleware!
*/
export const Scope = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const response = ctx.switchToHttp().getResponse();
const token = response.locals.accessToken as OAuth2AccessToken;
return token.scope;
},
);

View File

@ -0,0 +1,7 @@
import { SetMetadata } from '@nestjs/common';
/**
* Restrict this route to only these OAuth2 scopes. AND logic!
* @param scopes List of scopes for this route
*/
export const Scopes = (...scopes: string[]) => SetMetadata('scopes', scopes);

View File

@ -0,0 +1,11 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
/**
* Get the User from the current request. Requires a guard or middleware to inject the user.
*/
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);

View File

@ -0,0 +1,43 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { OAuth2Service } from 'src/modules/oauth2/oauth2.service';
import { UserService } from 'src/modules/objects/user/user.service';
/**
* Injects and validates OAuth2 bearer tokens.
*/
@Injectable()
export class OAuth2Guard implements CanActivate {
constructor(private _oauth2: OAuth2Service, private _user: UserService) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const http = context.switchToHttp();
const request = http.getRequest();
const response = http.getResponse();
return new Promise((resolve, reject) => {
try {
this._oauth2.oauth.bearer(request, response, (content) => {
if (content instanceof Error) {
return reject(content);
}
this._user
.getById(response.locals.accessToken.user_id, [
'picture',
'privileges',
])
.then((user) => {
request.user = user;
resolve(true);
})
.catch(reject);
});
} catch (e: any) {
reject(e);
}
});
}
}

View File

@ -0,0 +1,28 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
/**
* Validates privileges.
*/
@Injectable()
export class PrivilegesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const privileges = this.reflector.get<string[]>(
'privileges',
context.getHandler(),
);
if (!privileges) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return (
user.privileges.includes('*') ||
privileges.every((item) =>
user.privileges.find(({ name }) => name === item),
)
);
}
}

View File

@ -0,0 +1,26 @@
import { OAuth2AccessToken } from '@icynet/oauth2-provider';
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
/**
* Validates OAuth2 scopes.
*/
@Injectable()
export class ScopesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const scopes = this.reflector.get<string[]>('scopes', context.getHandler());
if (!scopes) {
return true;
}
const response = context.switchToHttp().getResponse();
const accessToken = response.locals.accessToken as OAuth2AccessToken;
if (!accessToken) {
return false;
}
return scopes.every((scope) => accessToken.scope.includes(scope));
}
}

View File

@ -15,9 +15,10 @@ async function bootstrap() {
const RedisStore = connectRedis(session); const RedisStore = connectRedis(session);
const redisClient = redis.createClient({ const redisClient = redis.createClient({
host: 'localhost', url: process.env.REDIS_URL || 'redis://localhost:6379',
port: 6379, legacyMode: true,
}); });
redisClient.connect();
// app.use(express.urlencoded()); // app.use(express.urlencoded());
app.use(cookieParser()); app.use(cookieParser());

View File

@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class AdminApiModule {}

View File

@ -0,0 +1,15 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { CurrentUser } from 'src/decorators/user.decorator';
import { OAuth2Guard } from 'src/guards/oauth2.guard';
import { User } from '../objects/user/user.entity';
@Controller({
path: '/api',
})
export class ApiController {
@Get('/')
@UseGuards(OAuth2Guard)
index(@CurrentUser() user: User) {
return { hello: true, user: user.username };
}
}

View File

@ -0,0 +1,26 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { ConfigurationModule } from '../config/config.module';
import { JWTModule } from '../jwt/jwt.module';
import { OAuth2Module } from '../oauth2/oauth2.module';
import { OAuth2Service } from '../oauth2/oauth2.service';
import { ObjectsModule } from '../objects/objects.module';
import { AdminApiModule } from './admin/admin.module';
import { ApiController } from './api.controller';
@Module({
controllers: [ApiController],
imports: [
ConfigurationModule,
JWTModule,
ObjectsModule,
AdminApiModule,
OAuth2Module,
],
})
export class ApiModule implements NestModule {
constructor(private _service: OAuth2Service) {}
configure(consumer: MiddlewareConsumer) {
consumer.apply(this._service.oauth.express()).forRoutes('/api*');
}
}

View File

@ -1,27 +0,0 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AuthMiddleware } from 'src/middleware/auth.middleware';
import { ValidateCSRFMiddleware } from 'src/middleware/validate-csrf.middleware';
import { OAuth2ClientModule } from 'src/modules/objects/oauth2-client/oauth2-client.module';
import { OAuth2TokenModule } from 'src/modules/objects/oauth2-token/oauth2-token.module';
import { UploadModule } from 'src/modules/objects/upload/upload.module';
import { UserModule } from 'src/modules/objects/user/user.module';
import { OAuth2Controller } from './oauth2.controller';
import { OAuth2Service } from './oauth2.service';
@Module({
imports: [UserModule, UploadModule, OAuth2ClientModule, OAuth2TokenModule],
controllers: [OAuth2Controller],
providers: [OAuth2Service],
exports: [OAuth2Service],
})
export class OAuth2Module implements NestModule {
constructor(private _service: OAuth2Service) {}
configure(consumer: MiddlewareConsumer) {
consumer.apply(this._service.oauth.express()).forRoutes('oauth2/*');
consumer.apply(this._service.oauth.bearer).forRoutes('oauth2/user');
consumer
.apply(AuthMiddleware, ValidateCSRFMiddleware)
.forRoutes('oauth2/authorize');
}
}

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { OAuth2ClientModule } from 'src/modules/objects/oauth2-client/oauth2-client.module';
import { OAuth2TokenModule } from 'src/modules/objects/oauth2-token/oauth2-token.module';
import { UploadModule } from 'src/modules/objects/upload/upload.module';
import { UserModule } from 'src/modules/objects/user/user.module';
import { OAuth2Service } from './oauth2.service';
@Module({
imports: [UserModule, UploadModule, OAuth2ClientModule, OAuth2TokenModule],
providers: [OAuth2Service],
exports: [OAuth2Service],
})
export class OAuth2Module {}

View File

@ -1,5 +1,5 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { DeleteResult, Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { User } from '../user/user.entity'; import { User } from '../user/user.entity';
import { OAuth2ClientAuthorization } from './oauth2-client-authorization.entity'; import { OAuth2ClientAuthorization } from './oauth2-client-authorization.entity';
import { import {
@ -56,8 +56,8 @@ export class OAuth2ClientService {
): Promise<OAuth2ClientAuthorization> { ): Promise<OAuth2ClientAuthorization> {
const existing = await this.clientAuthRepository.findOne({ const existing = await this.clientAuthRepository.findOne({
where: { where: {
user, user: { id: user.id },
client, client: { id: client.id },
}, },
relations: ['user', 'client'], relations: ['user', 'client'],
}); });
@ -87,7 +87,7 @@ export class OAuth2ClientService {
): Promise<OAuth2ClientAuthorization[]> { ): Promise<OAuth2ClientAuthorization[]> {
return this.clientAuthRepository.find({ return this.clientAuthRepository.find({
relations: ['user', 'client', 'client.urls'], relations: ['user', 'client', 'client.urls'],
where: { user }, where: { user: { id: user.id } },
}); });
} }
@ -104,7 +104,7 @@ export class OAuth2ClientService {
): Promise<OAuth2ClientAuthorization> { ): Promise<OAuth2ClientAuthorization> {
return this.clientAuthRepository.findOne({ return this.clientAuthRepository.findOne({
where: { where: {
user, user: { id: user.id },
id: authId, id: authId,
}, },
relations: ['user'], relations: ['user'],

View File

@ -0,0 +1,36 @@
import { Module } from '@nestjs/common';
import { ConfigurationModule } from '../config/config.module';
import { DatabaseModule } from './database/database.module';
import { DocumentModule } from './document/document.module';
import { EmailModule } from './email/email.module';
import { OAuth2ClientModule } from './oauth2-client/oauth2-client.module';
import { OAuth2TokenModule } from './oauth2-token/oauth2-token.module';
import { PrivilegeModule } from './privilege/privilege.module';
import { UploadModule } from './upload/upload.module';
import { UserModule } from './user/user.module';
@Module({
imports: [
ConfigurationModule,
DatabaseModule,
EmailModule,
OAuth2ClientModule,
OAuth2TokenModule,
PrivilegeModule,
UploadModule,
UserModule,
DocumentModule,
],
exports: [
DatabaseModule,
EmailModule,
OAuth2ClientModule,
OAuth2TokenModule,
PrivilegeModule,
UploadModule,
UserModule,
DocumentModule,
],
})
export class ObjectsModule {}

View File

@ -33,7 +33,7 @@ export class UserTOTPService {
*/ */
public async getUserTOTP(user: User): Promise<UserToken> { public async getUserTOTP(user: User): Promise<UserToken> {
return this.userTokenRepository.findOne({ return this.userTokenRepository.findOne({
where: { user, type: UserTokenType.TOTP }, where: { user: { id: user.id }, type: UserTokenType.TOTP },
}); });
} }

View File

@ -76,9 +76,11 @@ export class LoginController {
if (await this.totpService.userHasTOTP(user)) { if (await this.totpService.userHasTOTP(user)) {
const challenge = { type: 'verify', user: user.uuid, remember }; const challenge = { type: 'verify', user: user.uuid, remember };
req.session.challenge = await this.token.encryptChallenge(challenge); const encrypted = await this.token.encryptChallenge(challenge);
res.redirect( res.redirect(
'/login/verify' + (redirectTo ? '?redirectTo=' + redirectTo : ''), `login/verify?challenge=${encrypted}${
redirectTo ? '&redirectTo=' + redirectTo : ''
}`,
); );
return; return;
} }
@ -96,18 +98,19 @@ export class LoginController {
@Get('verify') @Get('verify')
public verifyUserTokenView( public verifyUserTokenView(
@Session() session: SessionData,
@Req() req: Request, @Req() req: Request,
@Res() res: Response, @Res() res: Response,
@Query('redirectTo') redirectTo?: string, @Query() query: { redirectTo: string; challenge: string },
) { ) {
if (!session.challenge) { if (!query.challenge) {
req.flash('message', { req.flash('message', {
error: true, error: true,
text: 'An unexpected error occured, please log in again.', text: 'An unexpected error occured, please log in again.',
}); });
res.redirect('/login' + (redirectTo ? '?redirectTo=' + redirectTo : '')); res.redirect(
'/login' + (query.redirectTo ? '?redirectTo=' + query.redirectTo : ''),
);
return; return;
} }
@ -120,34 +123,36 @@ export class LoginController {
@Body() body: { totp: string }, @Body() body: { totp: string },
@Req() req: Request, @Req() req: Request,
@Res() res: Response, @Res() res: Response,
@Query('redirectTo') redirectTo?: string, @Query() query: { redirectTo: string; challenge: string },
) { ) {
let user: User; let user: User;
let remember = false; let remember = false;
try { try {
if (!session.challenge) { if (!query.challenge) {
throw new Error('No challenge'); throw new Error('No challenge');
} }
const challenge = await this.token.decryptChallenge(session.challenge); const decrypted = await this.token.decryptChallenge(query.challenge);
if (!challenge || challenge.type !== 'verify' || !challenge.user) { if (!decrypted || decrypted.type !== 'verify' || !decrypted.user) {
throw new Error('Bad challenge'); throw new Error('Bad challenge');
} }
user = await this.userService.getByUUID(challenge.user); user = await this.userService.getByUUID(decrypted.user);
if (!user) { if (!user) {
throw new Error('Bad challenge'); throw new Error('Bad challenge');
} }
remember = challenge.remember; remember = decrypted.remember;
} catch (e: any) { } catch (e: any) {
req.flash('message', { req.flash('message', {
error: true, error: true,
text: 'An unexpected error occured, please log in again.', text: 'An unexpected error occured, please log in again.',
}); });
res.redirect('/login' + (redirectTo ? '?redirectTo=' + redirectTo : '')); res.redirect(
'/login' + (query.redirectTo ? '?redirectTo=' + query.redirectTo : ''),
);
return; return;
} }
@ -172,9 +177,8 @@ export class LoginController {
req.session.cookie.maxAge = month; req.session.cookie.maxAge = month;
} }
session.challenge = null;
session.user = user.uuid; session.user = user.uuid;
res.redirect(redirectTo ? decodeURIComponent(redirectTo) : '/'); res.redirect(query.redirectTo ? decodeURIComponent(query.redirectTo) : '/');
} }
@Get('activate') @Get('activate')
@ -231,7 +235,6 @@ export class LoginController {
public async recoverView( public async recoverView(
@Req() req: Request, @Req() req: Request,
@Res() res: Response, @Res() res: Response,
@Session() session: SessionData,
@Query() query: { token: string }, @Query() query: { token: string },
) { ) {
if (query.token) { if (query.token) {

View File

@ -1,4 +1,3 @@
import { OAuth2AccessToken } from '@icynet/oauth2-provider';
import { import {
Controller, Controller,
Get, Get,
@ -7,10 +6,15 @@ import {
Post, Post,
Req, Req,
Res, Res,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express'; import { NextFunction, Request, Response } from 'express';
import { Scope } from 'src/decorators/scope.decorator';
import { CurrentUser } from 'src/decorators/user.decorator';
import { OAuth2Guard } from 'src/guards/oauth2.guard';
import { ConfigurationService } from 'src/modules/config/config.service'; import { ConfigurationService } from 'src/modules/config/config.service';
import { OAuth2Service } from './oauth2.service'; import { User } from 'src/modules/objects/user/user.entity';
import { OAuth2Service } from '../../oauth2/oauth2.service';
@Controller('oauth2') @Controller('oauth2')
export class OAuth2Controller { export class OAuth2Controller {
@ -61,15 +65,11 @@ export class OAuth2Controller {
// TODO: Move to API // TODO: Move to API
@Get('user') @Get('user')
@UseGuards(OAuth2Guard)
public async userInfo( public async userInfo(
@Res({ passthrough: true }) res: Response, @CurrentUser() user: User,
@Scope() scope: string,
): Promise<Record<string, any>> { ): Promise<Record<string, any>> {
const token = res.locals.accessToken as OAuth2AccessToken;
const user = await this._service.userService.getById(
token.user_id as number,
['picture', 'privileges'],
);
if (!user) { if (!user) {
throw new NotFoundException('No such user'); throw new NotFoundException('No such user');
} }
@ -86,13 +86,13 @@ export class OAuth2Controller {
nickname: user.display_name, nickname: user.display_name,
}; };
if (token.scope.includes('email') || token.scope.includes('user:email')) { if (scope.includes('email') || scope.includes('user:email')) {
userData.email = user.email; userData.email = user.email;
userData.email_verified = true; userData.email_verified = true;
} }
if ( if (
(token.scope.includes('image') || token.scope.includes('user:image')) && (scope.includes('image') || scope.includes('user:image')) &&
user.picture user.picture
) { ) {
userData.image = `${this._config.get('app.base_url')}/uploads/${ userData.image = `${this._config.get('app.base_url')}/uploads/${
@ -102,10 +102,10 @@ export class OAuth2Controller {
} }
if ( if (
token.scope.includes('privileges') || scope.includes('privileges') ||
(token.scope.includes('user:privileges') && user.privileges?.length) (scope.includes('user:privileges') && user.privileges?.length)
) { ) {
userData.privileges = user.privileges; userData.privileges = user.privileges.map(({ name }) => name);
} }
return userData; return userData;

View File

@ -0,0 +1,22 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AuthMiddleware } from 'src/middleware/auth.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';
import { UserModule } from 'src/modules/objects/user/user.module';
import { OAuth2Controller } from './oauth2-router.controller';
@Module({
controllers: [OAuth2Controller],
imports: [OAuth2Module, UserModule],
})
export class OAuth2RouterModule implements NestModule {
constructor(private _service: OAuth2Service) {}
configure(consumer: MiddlewareConsumer) {
consumer.apply(this._service.oauth.express()).forRoutes('oauth2/*');
consumer
.apply(AuthMiddleware, ValidateCSRFMiddleware)
.forRoutes('oauth2/authorize');
}
}

View File

@ -15,7 +15,7 @@ import { ConfigurationModule } from 'src/modules/config/config.module';
import { ConfigurationService } from 'src/modules/config/config.service'; import { ConfigurationService } from 'src/modules/config/config.service';
import { UploadModule } from 'src/modules/objects/upload/upload.module'; import { UploadModule } from 'src/modules/objects/upload/upload.module';
import { UserModule } from 'src/modules/objects/user/user.module'; import { UserModule } from 'src/modules/objects/user/user.module';
import { OAuth2Module } from '../oauth2/oauth2.module'; import { OAuth2Module } from '../../oauth2/oauth2.module';
import { SettingsController } from './settings.controller'; import { SettingsController } from './settings.controller';
import { SettingsService } from './settings.service'; import { SettingsService } from './settings.service';
import { OAuth2ClientModule } from 'src/modules/objects/oauth2-client/oauth2-client.module'; import { OAuth2ClientModule } from 'src/modules/objects/oauth2-client/oauth2-client.module';

View File

@ -1,6 +1,6 @@
import { ConfigurationService } from 'src/modules/config/config.service'; import { ConfigurationService } from 'src/modules/config/config.service';
import { UserService } from 'src/modules/objects/user/user.service'; import { UserService } from 'src/modules/objects/user/user.service';
import { OAuth2Service } from '../oauth2/oauth2.service'; import { OAuth2Service } from '../../oauth2/oauth2.service';
export class SettingsService { export class SettingsService {
constructor( constructor(

View File

@ -0,0 +1,46 @@
import {
MiddlewareConsumer,
Module,
NestModule,
RequestMethod,
} from '@nestjs/common';
import { CSRFMiddleware } from 'src/middleware/csrf.middleware';
import { UserMiddleware } from 'src/middleware/user.middleware';
import { ConfigurationModule } from '../config/config.module';
import { JWTModule } from '../jwt/jwt.module';
import { ObjectsModule } from '../objects/objects.module';
import { LoginModule } from './login/login.module';
import { OAuth2RouterModule } from './oauth2-router/oauth2-router.module';
import { RegisterModule } from './register/register.module';
import { SettingsModule } from './settings/settings.module';
import { TwoFactorModule } from './two-factor/two-factor.module';
@Module({
imports: [
ConfigurationModule,
JWTModule,
ObjectsModule,
LoginModule,
OAuth2RouterModule,
RegisterModule,
SettingsModule,
TwoFactorModule,
],
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('*');
}
}

View File

@ -16,9 +16,9 @@ block body
.alert.alert-success .alert.alert-success
span #{message.text} span #{message.text}
form(method="post") form(method="post", autocomplete="off")
div.form-container div.form-container
input#csrf(type="hidden", name="_csrf", value=csrf) input#csrf(type="hidden", name="_csrf", value=csrf)
label.form-label(for="totp") Code label.form-label(for="totp") Code
input.form-control#totp(type="text", name="totp", autofocus, placeholder="xxxxxx") input.form-control#totp(type="text", autocomplete="off", name="totp", autofocus, placeholder="xxxxxx")
button.btn.btn-primary(type="submit") Log in button.btn.btn-primary(type="submit") Log in