First stable build with system check
This commit is contained in:
commit
b790a70d3a
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
data
|
||||||
|
!**/.gitkeep
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
type CloudDavContact {
|
||||||
|
urn: ID!
|
||||||
|
identityGroupUrn: String!
|
||||||
|
|
||||||
|
firstName: String
|
||||||
|
lastName: String
|
||||||
|
company: String
|
||||||
|
phones: [String]
|
||||||
|
addresses: [CloudDavContactAddress]
|
||||||
|
dates: [CloudDavContactLabeledString]
|
||||||
|
urls: [CloudDavContactLabeledString]
|
||||||
|
notes: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloudDavContactAddress {
|
||||||
|
urn: ID!
|
||||||
|
type: String
|
||||||
|
street1: String
|
||||||
|
street2: String
|
||||||
|
city: String
|
||||||
|
state: String
|
||||||
|
postalCode: String
|
||||||
|
country: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type CloudDavContactLabeledString {
|
||||||
|
context: String!
|
||||||
|
value: String!
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
type IdentityGroupToIdentityUserEdge {
|
||||||
|
data: [IdentityUser]
|
||||||
|
error: IdentityGroupToIdentityUserError
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdentityUserToIdentityGroupEdge {
|
||||||
|
data: [IdentityGroup]
|
||||||
|
error: IdentityUserToIdentityGroupError
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdentityUserToIdentityProfileEdge {
|
||||||
|
data: IdentityProfile
|
||||||
|
error: IdentityUserToIdentityProfileError
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdentityUserToIdentityEmailEdge {
|
||||||
|
data: [IdentityEmail]
|
||||||
|
error: IdentityUserToIdentityEmailError
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdentityUserToIdentityAuthDeviceEdge {
|
||||||
|
data: [IdentityAuthDevice]
|
||||||
|
error: IdentityUserToIdentityAuthDeviceError
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdentityGroupToCloudDavContactEdge {
|
||||||
|
data: [CloudDavContact]
|
||||||
|
error: IdentityGroupToCloudDavContactError
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
enum IdentityAuthDeviceTypeEnum {
|
||||||
|
PASSWORD
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
enum IdentityGroupToIdentityUserError {
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
enum IdentityUserToIdentityGroupError {
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
enum IdentityUserToIdentityProfileError {
|
||||||
|
UNKNOWN
|
||||||
|
NOT_FOUND
|
||||||
|
}
|
||||||
|
|
||||||
|
enum IdentityUserToIdentityEmailError {
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
enum IdentityUserToIdentityAuthDeviceError {
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
enum IdentityGroupToCloudDavContactError {
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
type IdentityGroup {
|
||||||
|
urn: ID!
|
||||||
|
isAdmin: Boolean!
|
||||||
|
name: String
|
||||||
|
|
||||||
|
Users: IdentityGroupToIdentityUserEdge!
|
||||||
|
Contacts: IdentityGroupToCloudDavContactEdge!
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdentityUser {
|
||||||
|
urn: ID!
|
||||||
|
externalId: String!
|
||||||
|
username: String!
|
||||||
|
|
||||||
|
Groups: IdentityUserToIdentityGroupEdge!
|
||||||
|
Profile: IdentityUserToIdentityProfileEdge!
|
||||||
|
Emails: IdentityUserToIdentityEmailEdge!
|
||||||
|
AuthDevices: IdentityUserToIdentityAuthDeviceEdge!
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdentityProfile {
|
||||||
|
urn: ID!
|
||||||
|
firstName: String
|
||||||
|
lastName: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdentityEmail {
|
||||||
|
urn: ID!
|
||||||
|
email: String!
|
||||||
|
userUrn: String!
|
||||||
|
verified: Boolean!
|
||||||
|
default: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdentityAuthDevice {
|
||||||
|
urn: ID!
|
||||||
|
userUrn: String!
|
||||||
|
deviceType: IdentityAuthDeviceTypeEnum!
|
||||||
|
|
||||||
|
IdentityAuthDevicePassword: IdentityAuthDevicePassword
|
||||||
|
}
|
||||||
|
|
||||||
|
type IdentityAuthDevicePassword {
|
||||||
|
urn: ID!
|
||||||
|
expiry: String!
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
enum SystemSettingHashValueTypeEnum {
|
||||||
|
BOOLEAN
|
||||||
|
STRING
|
||||||
|
NUMBER
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
enum SystemSettingsQueryOutputError {
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UpdateSystemSettingOutputError {
|
||||||
|
UNKNOWN
|
||||||
|
NOT_FOUND
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
input UpdateSystemSettingInput {
|
||||||
|
urn: ID!
|
||||||
|
hashValue: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateSystemSettingOutput {
|
||||||
|
error: UpdateSystemSettingOutputError
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
updateSystemSetting(input: UpdateSystemSettingInput!): UpdateSystemSettingOutput!
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
type SystemSettingsQueryOutput {
|
||||||
|
data: [SystemSetting]
|
||||||
|
error: SystemSettingsQueryOutputError
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
systemSettings: SystemSettingsQueryOutput!
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
type SystemSetting {
|
||||||
|
urn: ID!
|
||||||
|
hashKey: String!
|
||||||
|
hashValueType: SystemSettingHashValueTypeEnum!
|
||||||
|
hashValue: String!
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
__generated__
|
||||||
|
graphql.schema.json
|
||||||
|
dist
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Namespacing
|
||||||
|
- Database and Graphql is namespaced by prefixed camelcase
|
||||||
|
- Typescript is namespaced by `namespace` keyword
|
||||||
|
|
||||||
|
|
||||||
|
# Adding to the enumeration types
|
||||||
|
- Update graphql enum
|
||||||
|
- Update services/core/enumerations namespaced types
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
<p align="center">
|
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||||
|
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
||||||
|
</p>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](https://opencollective.com/nest#sponsor)-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the app
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ npm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ npm run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ npm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||||
|
|
||||||
|
## Stay in touch
|
||||||
|
|
||||||
|
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
|
||||||
|
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Nest is [MIT licensed](LICENSE).
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
|
||||||
|
import type { CodegenConfig } from '@graphql-codegen/cli';
|
||||||
|
|
||||||
|
const config: CodegenConfig = {
|
||||||
|
overwrite: true,
|
||||||
|
schema: "../../graphql",
|
||||||
|
generates: {
|
||||||
|
"src/__generated__/graphql.ts": {
|
||||||
|
plugins: ["typescript", "typescript-resolvers"]
|
||||||
|
},
|
||||||
|
"./graphql.schema.json": {
|
||||||
|
plugins: ["introspection"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true,
|
||||||
|
"plugins": ["@nestjs/graphql"]
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,85 @@
|
||||||
|
{
|
||||||
|
"name": "@homelab-personal-cloud/core",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
|
"generate": "graphql-codegen --config codegen.ts",
|
||||||
|
"migrate": "npx prisma db push",
|
||||||
|
"migrate:post": "nest start --entryFile prisma-post-migrations"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@apollo/server": "^4.10.2",
|
||||||
|
"@nestjs/apollo": "^12.1.0",
|
||||||
|
"@nestjs/common": "^10.0.0",
|
||||||
|
"@nestjs/core": "^10.0.0",
|
||||||
|
"@nestjs/graphql": "^12.1.1",
|
||||||
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
|
"@prisma/client": "^5.12.1",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"graphql": "^16.8.1",
|
||||||
|
"joi": "^17.12.3",
|
||||||
|
"reflect-metadata": "^0.2.0",
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@graphql-codegen/cli": "^5.0.2",
|
||||||
|
"@graphql-codegen/introspection": "4.0.3",
|
||||||
|
"@graphql-codegen/typescript": "^4.0.6",
|
||||||
|
"@graphql-codegen/typescript-resolvers": "^4.0.6",
|
||||||
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
"@nestjs/schematics": "^10.0.0",
|
||||||
|
"@nestjs/testing": "^10.0.0",
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/jest": "^29.5.2",
|
||||||
|
"@types/node": "^20.3.1",
|
||||||
|
"@types/supertest": "^6.0.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
|
"eslint": "^8.42.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
|
"jest": "^29.5.0",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"prisma": "^5.12.1",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^6.3.3",
|
||||||
|
"ts-jest": "^29.1.0",
|
||||||
|
"ts-loader": "^9.4.3",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.1.3"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "SystemSetting" (
|
||||||
|
"hashKey" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"hashValue" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "SystemPostMigration" (
|
||||||
|
"name" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "IdentityGroup" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"isAdmin" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"name" TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "IdentityGroupToIdentityUser" (
|
||||||
|
"groupId" INTEGER NOT NULL,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY ("groupId", "userId"),
|
||||||
|
CONSTRAINT "IdentityGroupToIdentityUser_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "IdentityGroup" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "IdentityGroupToIdentityUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "IdentityUser" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "IdentityUser" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"externalId" TEXT NOT NULL,
|
||||||
|
"username" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "IdentityProfileNonNormalized" (
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"hashKey" TEXT NOT NULL,
|
||||||
|
"hashValue" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
PRIMARY KEY ("userId", "hashKey"),
|
||||||
|
CONSTRAINT "IdentityProfileNonNormalized_userId_fkey" FOREIGN KEY ("userId") REFERENCES "IdentityUser" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "IdentityUserEmails" (
|
||||||
|
"email" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"verified" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"default" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
CONSTRAINT "IdentityUserEmails_userId_fkey" FOREIGN KEY ("userId") REFERENCES "IdentityUser" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "EnumIdentityAuthDeviceType" (
|
||||||
|
"enumValue" TEXT NOT NULL PRIMARY KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "IdentityAuthDevice" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"deviceType" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "IdentityAuthDevice_userId_fkey" FOREIGN KEY ("userId") REFERENCES "IdentityUser" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "IdentityAuthDevice_deviceType_fkey" FOREIGN KEY ("deviceType") REFERENCES "EnumIdentityAuthDeviceType" ("enumValue") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "IdentityAuthDeviceNonNormalized" (
|
||||||
|
"authDeviceId" INTEGER NOT NULL,
|
||||||
|
"hashKey" TEXT NOT NULL,
|
||||||
|
"hashValue" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
PRIMARY KEY ("authDeviceId", "hashKey"),
|
||||||
|
CONSTRAINT "IdentityAuthDeviceNonNormalized_authDeviceId_fkey" FOREIGN KEY ("authDeviceId") REFERENCES "IdentityAuthDevice" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "EnumCloudDavResourceType" (
|
||||||
|
"enumValue" TEXT NOT NULL PRIMARY KEY
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CloudDavResource" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"identityGroupId" INTEGER NOT NULL,
|
||||||
|
"resourceType" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "CloudDavResource_identityGroupId_fkey" FOREIGN KEY ("identityGroupId") REFERENCES "IdentityGroup" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "CloudDavResource_resourceType_fkey" FOREIGN KEY ("resourceType") REFERENCES "EnumCloudDavResourceType" ("enumValue") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CloudDavResourceNonNormalized" (
|
||||||
|
"davResourceId" TEXT NOT NULL,
|
||||||
|
"hashKey" TEXT NOT NULL,
|
||||||
|
"hashValue" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
PRIMARY KEY ("davResourceId", "hashKey"),
|
||||||
|
CONSTRAINT "CloudDavResourceNonNormalized_davResourceId_fkey" FOREIGN KEY ("davResourceId") REFERENCES "CloudDavResource" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "IdentityUser_externalId_key" ON "IdentityUser"("externalId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "IdentityUser_username_key" ON "IdentityUser"("username");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "IdentityAuthDevice_userId_idx" ON "IdentityAuthDevice"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "IdentityAuthDevice_userId_deviceType_idx" ON "IdentityAuthDevice"("userId", "deviceType");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "CloudDavResource_identityGroupId_idx" ON "CloudDavResource"("identityGroupId");
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "sqlite"
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = "file:../../../data/core.db"
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Namespace: System
|
||||||
|
//
|
||||||
|
model SystemSetting {
|
||||||
|
hashKey String @id
|
||||||
|
hashValue String
|
||||||
|
}
|
||||||
|
|
||||||
|
model SystemPostMigration {
|
||||||
|
name String @id
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Namespace: Identity
|
||||||
|
//
|
||||||
|
model IdentityGroup {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
isAdmin Boolean @default(false)
|
||||||
|
name String?
|
||||||
|
|
||||||
|
users IdentityGroupToIdentityUser[]
|
||||||
|
davResources CloudDavResource[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model IdentityGroupToIdentityUser {
|
||||||
|
groupId Int
|
||||||
|
group IdentityGroup @relation(fields: [groupId], references: [id])
|
||||||
|
userId Int
|
||||||
|
user IdentityUser @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
|
@@id([groupId, userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model IdentityUser {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
externalId String @unique @default(uuid())
|
||||||
|
username String @unique
|
||||||
|
|
||||||
|
groups IdentityGroupToIdentityUser[]
|
||||||
|
profileHashMapPairs IdentityProfileNonNormalized[]
|
||||||
|
emails IdentityUserEmails[]
|
||||||
|
authDevices IdentityAuthDevice[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model IdentityProfileNonNormalized {
|
||||||
|
userId Int
|
||||||
|
user IdentityUser @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
|
hashKey String
|
||||||
|
hashValue String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@id([userId, hashKey])
|
||||||
|
}
|
||||||
|
|
||||||
|
model IdentityUserEmails {
|
||||||
|
email String @id
|
||||||
|
|
||||||
|
userId Int
|
||||||
|
user IdentityUser @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
|
verified Boolean @default(false)
|
||||||
|
default Boolean @default(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
model EnumIdentityAuthDeviceType {
|
||||||
|
enumValue String @id
|
||||||
|
|
||||||
|
authDevices IdentityAuthDevice[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model IdentityAuthDevice {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
|
||||||
|
userId Int
|
||||||
|
user IdentityUser @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
|
deviceType String
|
||||||
|
deviceTypeRelation EnumIdentityAuthDeviceType @relation(fields: [deviceType], references: [enumValue])
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
hashMapPairs IdentityAuthDeviceNonNormalized[]
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([userId, deviceType])
|
||||||
|
}
|
||||||
|
|
||||||
|
model IdentityAuthDeviceNonNormalized {
|
||||||
|
authDeviceId Int
|
||||||
|
davResource IdentityAuthDevice @relation(fields: [authDeviceId], references: [id])
|
||||||
|
|
||||||
|
hashKey String
|
||||||
|
hashValue String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@id([authDeviceId, hashKey])
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Namespace: cloud-dav
|
||||||
|
//
|
||||||
|
model EnumCloudDavResourceType {
|
||||||
|
enumValue String @id
|
||||||
|
|
||||||
|
davResources CloudDavResource[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model CloudDavResource {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
identityGroupId Int
|
||||||
|
IdentityGroup IdentityGroup @relation(fields: [identityGroupId], references: [id])
|
||||||
|
|
||||||
|
resourceType String
|
||||||
|
resourceTypeRelation EnumCloudDavResourceType @relation(fields: [resourceType], references: [enumValue])
|
||||||
|
|
||||||
|
hashMapPairs CloudDavResourceNonNormalized[]
|
||||||
|
|
||||||
|
@@index([identityGroupId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model CloudDavResourceNonNormalized {
|
||||||
|
davResourceId String
|
||||||
|
davResource CloudDavResource @relation(fields: [davResourceId], references: [id])
|
||||||
|
|
||||||
|
hashKey String
|
||||||
|
hashValue String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@id([davResourceId, hashKey])
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
import { ConfigModule } from './config/config.module';
|
||||||
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
|
import { GraphqlModule } from './graphql/graphql.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
AuthModule,
|
||||||
|
ConfigModule,
|
||||||
|
PrismaModule,
|
||||||
|
GraphqlModule,
|
||||||
|
],
|
||||||
|
controllers: [],
|
||||||
|
providers: [],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({})
|
||||||
|
export class AuthModule {}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigService } from './config.service';
|
||||||
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [ConfigService],
|
||||||
|
exports: [ConfigService],
|
||||||
|
})
|
||||||
|
export class ConfigModule {}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { normalizePairs } from '../utils';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { Config, configValidator} from './config.struct';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ConfigService {
|
||||||
|
|
||||||
|
private readonly _config: Promise<Config>;;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
) {
|
||||||
|
|
||||||
|
this._config = new Promise(async (resolve, reject) => {
|
||||||
|
|
||||||
|
const settingsHashMap = await this.prismaService.systemSetting.findMany();
|
||||||
|
const settingsObject = normalizePairs(settingsHashMap);
|
||||||
|
const { value: config, error } = configValidator.validate(settingsObject, { abortEarly: false, allowUnknown: false });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(config);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<K extends keyof Config>(key: K): Promise<Config[K]> {
|
||||||
|
const config = await this._config;
|
||||||
|
return config[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
|
||||||
|
import { SystemSettings } from '../enumerations';
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
[SystemSettings.Graphql.Debug]: boolean;
|
||||||
|
[SystemSettings.Graphql.IntrospectionEnabled]: boolean;
|
||||||
|
[SystemSettings.Graphql.PlaygroundEnabled]: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const configValidator: Joi.ObjectSchema<Config> = Joi.object<Config, true>({
|
||||||
|
'graphql.debug.enabled': Joi.boolean().required(),
|
||||||
|
'graphql.introspection.enabled': Joi.boolean().required(),
|
||||||
|
'graphql.playground.enabled': Joi.boolean().required(),
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
export namespace CloudDav {
|
||||||
|
|
||||||
|
export namespace Resource {
|
||||||
|
export enum Type {
|
||||||
|
Contact = 'contact'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
export namespace Identity {
|
||||||
|
|
||||||
|
export namespace AuthDevice {
|
||||||
|
export enum Type {
|
||||||
|
Password = 'password',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PasswordHashKey {
|
||||||
|
Expiry = 'expiry',
|
||||||
|
PasswordHashString = 'password_hash_string',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './cloud-dav.enumerations';
|
||||||
|
export * from './identity.enumerations';
|
||||||
|
export * from './system-settings.enumerations';
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export namespace SystemSettings {
|
||||||
|
export enum Graphql {
|
||||||
|
Debug = 'graphql.debug.enabled',
|
||||||
|
IntrospectionEnabled = 'graphql.introspection.enabled',
|
||||||
|
PlaygroundEnabled = 'graphql.playground.enabled',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { GraphQLModule } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { ConfigService } from '../config/config.service';
|
||||||
|
import { SystemSettings } from '../enumerations';
|
||||||
|
import { SystemSettingsResolver } from './system-settings.resolver';
|
||||||
|
import { PrismaModule } from '../prisma/prisma.module';
|
||||||
|
import { ConfigModule } from '../config/config.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule,
|
||||||
|
PrismaModule,
|
||||||
|
GraphQLModule.forRootAsync<ApolloDriverConfig>({
|
||||||
|
driver: ApolloDriver,
|
||||||
|
useFactory: async (configService: ConfigService) => ({
|
||||||
|
debug: await configService.get(SystemSettings.Graphql.Debug),
|
||||||
|
playground: await configService.get(SystemSettings.Graphql.PlaygroundEnabled),
|
||||||
|
introspection: await configService.get(SystemSettings.Graphql.IntrospectionEnabled),
|
||||||
|
typePaths: ['../../graphql/**/*.graphql'],
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
imports: [ConfigModule],
|
||||||
|
})
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
SystemSettingsResolver,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class GraphqlModule {}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { SystemSettingHashValueTypeEnum, SystemSettingsQueryOutput, SystemSettingsQueryOutputError, UpdateSystemSettingInput, UpdateSystemSettingOutput, UpdateSystemSettingOutputError } from '../__generated__/graphql';
|
||||||
|
import { normalizePairs } from '../utils';
|
||||||
|
|
||||||
|
const getUrn = (hashKey: string): string => `urn:system-settings:${hashKey}`;
|
||||||
|
const parseUrn = (urn: string): string => urn.split(':').pop();
|
||||||
|
|
||||||
|
@Resolver('SystemSettings')
|
||||||
|
export class SystemSettingsResolver {
|
||||||
|
|
||||||
|
private readonly logger: Logger = new Logger(SystemSettingsResolver.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Query()
|
||||||
|
async systemSettings(): Promise<SystemSettingsQueryOutput> {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const systemSettingsPairs = await this.prismaService.systemSetting.findMany();
|
||||||
|
const systemSettingsHashSet = normalizePairs(systemSettingsPairs);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: Object.keys(systemSettingsHashSet).map(hashKey => ({
|
||||||
|
urn: getUrn(hashKey),
|
||||||
|
hashKey,
|
||||||
|
hashValue: systemSettingsHashSet[hashKey].toString(),
|
||||||
|
hashValueType: this.determineHashValueType(systemSettingsHashSet[hashKey]),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(e.message);
|
||||||
|
return {
|
||||||
|
error: SystemSettingsQueryOutputError.Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation()
|
||||||
|
async updateSystemSetting(
|
||||||
|
@Args('input') { urn, hashValue }: UpdateSystemSettingInput
|
||||||
|
): Promise<UpdateSystemSettingOutput> {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const hashKey = parseUrn(urn);
|
||||||
|
|
||||||
|
const existing = await this.prismaService.systemSetting.findUnique({
|
||||||
|
where: {
|
||||||
|
hashKey
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return {
|
||||||
|
error: UpdateSystemSettingOutputError.NotFound,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prismaService.systemSetting.update({
|
||||||
|
data: { hashValue },
|
||||||
|
where: { hashKey },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(e.message);
|
||||||
|
return {
|
||||||
|
error: UpdateSystemSettingOutputError.Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private determineHashValueType = (hashValue: string | number | boolean): SystemSettingHashValueTypeEnum => {
|
||||||
|
|
||||||
|
if (typeof hashValue === 'number') {
|
||||||
|
return SystemSettingHashValueTypeEnum.Number;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof hashValue === 'boolean') {
|
||||||
|
return SystemSettingHashValueTypeEnum.Boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SystemSettingHashValueTypeEnum.String;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
await app.listen(3000);
|
||||||
|
}
|
||||||
|
bootstrap();
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { NestFactory, repl } from '@nestjs/core';
|
||||||
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
|
import { PrismaService } from './prisma/prisma.service';
|
||||||
|
import { DataMigration } from './prisma/data-migrations/data-migration.interface';
|
||||||
|
import { _00ApplicationBootstrapDataMigration } from './prisma/data-migrations';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(PrismaModule);
|
||||||
|
const prismaService = app.get(PrismaService);
|
||||||
|
|
||||||
|
const registeredMigrations: DataMigration[] = [
|
||||||
|
new _00ApplicationBootstrapDataMigration,
|
||||||
|
];
|
||||||
|
|
||||||
|
const alreadyRan = await prismaService.systemPostMigration.findMany();
|
||||||
|
const alreadyRanList = alreadyRan.map(e => e.name);
|
||||||
|
|
||||||
|
for (const migration of registeredMigrations) {
|
||||||
|
if (alreadyRanList.includes(migration.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await migration.run(prismaService);
|
||||||
|
await prismaService.systemPostMigration.create({ data: { name: migration.name }});
|
||||||
|
alreadyRanList.push(migration.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bootstrap();
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { Identity, CloudDav, SystemSettings } from '../../enumerations';
|
||||||
|
import { SecureStringUtil, denormalizeIntoPairs } from '../../utils';
|
||||||
|
import { PrismaService } from '../prisma.service';
|
||||||
|
import { DataMigration } from './data-migration.interface';
|
||||||
|
|
||||||
|
export class _00ApplicationBootstrapDataMigration implements DataMigration {
|
||||||
|
|
||||||
|
readonly name = '00-application-bootstrap-data-migration';
|
||||||
|
|
||||||
|
async run(prisma: PrismaService) {
|
||||||
|
await prisma.$queryRaw`PRAGMA journal_mode=WAL;`;
|
||||||
|
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
|
||||||
|
await tx.enumIdentityAuthDeviceType.create({
|
||||||
|
data: {
|
||||||
|
enumValue: Identity.AuthDevice.Type.Password,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.enumCloudDavResourceType.create({
|
||||||
|
data: {
|
||||||
|
enumValue: CloudDav.Resource.Type.Contact,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const adminGroup = await tx.identityGroup.create({
|
||||||
|
data: {
|
||||||
|
isAdmin: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const adminUser = await tx.identityUser.create({
|
||||||
|
data: {
|
||||||
|
username: 'admin',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.identityGroupToIdentityUser.create({
|
||||||
|
data: {
|
||||||
|
userId: adminUser.id,
|
||||||
|
groupId: adminGroup.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.identityAuthDevice.create({
|
||||||
|
data: {
|
||||||
|
userId: adminUser.id,
|
||||||
|
deviceType: Identity.AuthDevice.Type.Password,
|
||||||
|
hashMapPairs: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
hashKey: Identity.AuthDevice.PasswordHashKey.Expiry,
|
||||||
|
hashValue: '0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hashKey: Identity.AuthDevice.PasswordHashKey.PasswordHashString,
|
||||||
|
hashValue: await SecureStringUtil.generateNewHash('thiswillrequirechanging'),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const startingConfig = {
|
||||||
|
[SystemSettings.Graphql.Debug]: false,
|
||||||
|
[SystemSettings.Graphql.IntrospectionEnabled]: true,
|
||||||
|
[SystemSettings.Graphql.PlaygroundEnabled]: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const configData = denormalizeIntoPairs(startingConfig);
|
||||||
|
|
||||||
|
await tx.systemSetting.createMany({
|
||||||
|
data: configData,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { PrismaService } from '../prisma.service';
|
||||||
|
|
||||||
|
export interface DataMigration {
|
||||||
|
name: string;
|
||||||
|
run(prisma: PrismaService): void;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './00-application-bootstrap-data-migration';
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PrismaService } from './prisma.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
PrismaService,
|
||||||
|
],
|
||||||
|
exports: [PrismaService],
|
||||||
|
})
|
||||||
|
export class PrismaModule {}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
import { _00ApplicationBootstrapDataMigration } from './data-migrations';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.$connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
await this.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './normalize-hash-set';
|
||||||
|
export * from './secure-string';
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
interface Pairs {
|
||||||
|
hashKey: string;
|
||||||
|
hashValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizePairs = (pairs: Pairs[]): Record<string, string | number | boolean> => {
|
||||||
|
return pairs.reduce((acc, {hashKey, hashValue}) => {
|
||||||
|
acc[hashKey] = coerceTypeByPrefix(hashValue);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const coerceTypeByPrefix = (hashValue: string): string | number | boolean => {
|
||||||
|
if (hashValue.startsWith(Prefix.Number)) {
|
||||||
|
return Number(hashValue.substring(Prefix.Number.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashValue.startsWith(Prefix.Boolean)) {
|
||||||
|
return Boolean(hashValue.substring(Prefix.Boolean.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashValue.startsWith(Prefix.String)) {
|
||||||
|
return String(hashValue.substring(Prefix.String.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const denormalizeIntoPairs = (obj: Record<string, string | number | boolean>): Pairs[] => {
|
||||||
|
return Object.keys(obj).map(hashKey => {
|
||||||
|
|
||||||
|
const type = determineTypePrefix(obj[hashKey]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hashKey,
|
||||||
|
hashValue: `${type}:${obj[hashKey].toString()}`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const determineTypePrefix = (hashValue: string | number | boolean): Prefix => {
|
||||||
|
|
||||||
|
if (typeof hashValue === 'number') {
|
||||||
|
return Prefix.Number;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof hashValue === 'boolean') {
|
||||||
|
return Prefix.Boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Prefix.String;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Prefix {
|
||||||
|
String = 'string:',
|
||||||
|
Boolean = 'boolean:',
|
||||||
|
Number = 'number:',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
// https://passlib.readthedocs.io/en/stable/modular_crypt_format.html#mcf-identifiers
|
||||||
|
type OSDefinedHashPrefix = |
|
||||||
|
'$1$' |
|
||||||
|
'$2$' | '$2a$' | '$2x$' | '$2y$' | '$2b$' |
|
||||||
|
'$5$' |
|
||||||
|
'$6$'
|
||||||
|
;
|
||||||
|
|
||||||
|
enum NodeCryptoAlgs {
|
||||||
|
md5 = 'md5',
|
||||||
|
bcrypt = 'bcrypt',
|
||||||
|
sha256 = 'sha256',
|
||||||
|
sha512 = 'sha512',
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashDictionary: Record<OSDefinedHashPrefix, NodeCryptoAlgs> = {
|
||||||
|
$1$: NodeCryptoAlgs.md5,
|
||||||
|
$2$: NodeCryptoAlgs.bcrypt,
|
||||||
|
$2a$: NodeCryptoAlgs.bcrypt,
|
||||||
|
$2x$: NodeCryptoAlgs.bcrypt,
|
||||||
|
$2y$: NodeCryptoAlgs.bcrypt,
|
||||||
|
$2b$: NodeCryptoAlgs.bcrypt,
|
||||||
|
$5$: NodeCryptoAlgs.sha256,
|
||||||
|
$6$: NodeCryptoAlgs.sha512,
|
||||||
|
}
|
||||||
|
|
||||||
|
const inversedHashDictionary: Record<NodeCryptoAlgs, OSDefinedHashPrefix> = {
|
||||||
|
[NodeCryptoAlgs.md5]: '$1$',
|
||||||
|
[NodeCryptoAlgs.bcrypt]: '$2$',
|
||||||
|
[NodeCryptoAlgs.sha256]: '$5$',
|
||||||
|
[NodeCryptoAlgs.sha512]: '$6$',
|
||||||
|
}
|
||||||
|
|
||||||
|
const saltStringLengthDictionary: Record<NodeCryptoAlgs, number> = {
|
||||||
|
[NodeCryptoAlgs.md5]: 23,
|
||||||
|
[NodeCryptoAlgs.bcrypt]: 23,
|
||||||
|
[NodeCryptoAlgs.sha256]: 23,
|
||||||
|
[NodeCryptoAlgs.sha512]: 23,
|
||||||
|
}
|
||||||
|
|
||||||
|
type HashingFunction = (rawString: string, workFactor: number, salt: string) => Promise<string>;
|
||||||
|
|
||||||
|
const algImplementationMap: Record<NodeCryptoAlgs, HashingFunction> = {
|
||||||
|
[NodeCryptoAlgs.md5]: async function (rawString: string, workFactor: number, salt: string): Promise<string> {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
[NodeCryptoAlgs.bcrypt]: async function (rawString: string, _workFactor: number, salt: string): Promise<string> {
|
||||||
|
return bcrypt.hash(rawString, salt);
|
||||||
|
},
|
||||||
|
[NodeCryptoAlgs.sha256]: async function (rawString: string, workFactor: number, salt: string): Promise<string> {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
},
|
||||||
|
[NodeCryptoAlgs.sha512]: async function (rawString: string, workFactor: number, salt: string): Promise<string> {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SecureStringUtil {
|
||||||
|
|
||||||
|
private static readonly defaultWorkFactor = 15;
|
||||||
|
|
||||||
|
static async generateNewHash(rawString: string): Promise<string> {
|
||||||
|
|
||||||
|
const alg = NodeCryptoAlgs.bcrypt;
|
||||||
|
const workFactor = this.defaultWorkFactor;
|
||||||
|
const salt = await bcrypt.genSalt(workFactor);
|
||||||
|
return algImplementationMap[alg](rawString, workFactor, salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async generateHash(rawString: string, alg: NodeCryptoAlgs, workFactor: number, salt: string): Promise<string> {
|
||||||
|
return algImplementationMap[alg](rawString, workFactor, salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async compare(rawString: string, hashMetaSerialized: string): Promise<boolean> {
|
||||||
|
|
||||||
|
const { alg, salt, workFactor, hash: hashA} = HashMeta.deserialize(hashMetaSerialized);
|
||||||
|
const hashB = await this.generateHash(rawString, alg, workFactor, salt);
|
||||||
|
return hashA === hashB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HashMeta {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly alg: NodeCryptoAlgs,
|
||||||
|
public readonly workFactor: number,
|
||||||
|
public readonly salt: string,
|
||||||
|
public readonly hash: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
static deserialize(hashMeta: string): HashMeta {
|
||||||
|
|
||||||
|
const [_, alg, workFactor, salthash] = hashMeta.split('$');
|
||||||
|
|
||||||
|
const saltStringLength = saltStringLengthDictionary[alg];
|
||||||
|
const salt = salthash.substring(0, saltStringLength);
|
||||||
|
const hash = salthash.substring(saltStringLength);
|
||||||
|
|
||||||
|
return new HashMeta(
|
||||||
|
hashDictionary[alg],
|
||||||
|
+workFactor,
|
||||||
|
salt,
|
||||||
|
hash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(): string {
|
||||||
|
return [
|
||||||
|
'',
|
||||||
|
inversedHashDictionary[this.alg],
|
||||||
|
this.workFactor,
|
||||||
|
`${this.salt}${this.hash}`
|
||||||
|
].join('$');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import * as request from 'supertest';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2021",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue