This commit is contained in:
Matthew Bessette 2024-04-21 06:37:19 +00:00
parent 2410ef056c
commit 59540510b7
97 changed files with 1930 additions and 1029 deletions

View File

@ -11,7 +11,7 @@ type AuthOauth2Client {
clientSecret: String clientSecret: String
Realm: AuthOauth2ClientToAuthRealmEdge Realm: AuthOauth2ClientToAuthRealmEdge
Scopes: AuthOauth2ClientToAuthOauth2Scopes Scopes: AuthOauth2ClientToAuthOauth2ScopesEdge
} }
type AuthOauth2Scope { type AuthOauth2Scope {

View File

@ -1,10 +1,10 @@
type IdentityUserOutput { type IdentityUserOutput {
error: error: String
data: IdentityUser data: IdentityUser
} }
type Query { # type Query {
identityUsers() # # identityUsers()
identityUser(urn: String!): IdentityUserOutput! # # identityUser(urn: String!): IdentityUserOutput!
myUser # # myUser
} # }

View File

@ -1,5 +1,6 @@
input UpdateSystemSettingInput { input UpdateSystemSettingInput {
urn: ID! urn: ID!
hashValueType: SystemSettingHashValueTypeEnum!
hashValue: String! hashValue: String!
} }

View File

@ -1,5 +1,5 @@
node_modules node_modules
.env .env
__generated__ .generated
graphql.schema.json graphql.schema.json
dist dist

View File

@ -3,9 +3,9 @@ import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = { const config: CodegenConfig = {
overwrite: true, overwrite: true,
schema: "../../graphql", schema: "../graphql",
generates: { generates: {
"src/__generated__/graphql.ts": { "src/.generated/graphql.ts": {
plugins: ["typescript", "typescript-resolvers"] plugins: ["typescript", "typescript-resolvers"]
}, },
"./graphql.schema.json": { "./graphql.schema.json": {

View File

@ -11,16 +11,15 @@
"dependencies": { "dependencies": {
"@apollo/server": "^4.10.2", "@apollo/server": "^4.10.2",
"@nestjs/apollo": "^12.1.0", "@nestjs/apollo": "^12.1.0",
"@nestjs/cache-manager": "^2.2.2",
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
"@nestjs/graphql": "^12.1.1", "@nestjs/graphql": "^12.1.1",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"@prisma/client": "^5.12.1", "@prisma/client": "^5.12.1",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cache-manager": "^5.5.1",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"joi": "17.6.4", "joi": "17.6.4",
"pug": "^3.0.2",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1" "rxjs": "^7.8.1"
}, },
@ -1001,7 +1000,6 @@
"version": "7.24.1", "version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
"integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
"dev": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@ -1010,7 +1008,6 @@
"version": "7.22.20", "version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"dev": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@ -1128,7 +1125,6 @@
"version": "7.24.4", "version": "7.24.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz",
"integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==",
"dev": true,
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
}, },
@ -1769,7 +1765,6 @@
"version": "7.24.0", "version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
"integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
"dev": true,
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.23.4", "@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20", "@babel/helper-validator-identifier": "^7.22.20",
@ -4317,17 +4312,6 @@
} }
} }
}, },
"node_modules/@nestjs/cache-manager": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.2.2.tgz",
"integrity": "sha512-+n7rpU1QABeW2WV17Dl1vZCG3vWjJU1MaamWgZvbGxYE9EeCM0lVLfw3z7acgDTNwOy+K68xuQPoIMxD0bhjlA==",
"peerDependencies": {
"@nestjs/common": "^9.0.0 || ^10.0.0",
"@nestjs/core": "^9.0.0 || ^10.0.0",
"cache-manager": "<=5",
"rxjs": "^7.0.0"
}
},
"node_modules/@nestjs/cli": { "node_modules/@nestjs/cli": {
"version": "10.3.2", "version": "10.3.2",
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz",
@ -5959,8 +5943,7 @@
"node_modules/asap": { "node_modules/asap": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
"dev": true
}, },
"node_modules/asn1js": { "node_modules/asn1js": {
"version": "3.0.5", "version": "3.0.5",
@ -5976,6 +5959,11 @@
"node": ">=12.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/assert-never": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
"integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw=="
},
"node_modules/astral-regex": { "node_modules/astral-regex": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
@ -6170,6 +6158,17 @@
"@babel/core": "^7.0.0" "@babel/core": "^7.0.0"
} }
}, },
"node_modules/babel-walk": {
"version": "3.0.0-canary-5",
"resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
"integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==",
"dependencies": {
"@babel/types": "^7.9.6"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/backo2": { "node_modules/backo2": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
@ -6406,30 +6405,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/cache-manager": {
"version": "5.5.1",
"resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.5.1.tgz",
"integrity": "sha512-QYZFOjZTTennYdN3NNCKh+yq452+wQ4ChyL40jkEyghIgg5Ugwb4YO8ARIIF1fvTBkgDLlLTYFaxZVaPGmQ92A==",
"dependencies": {
"eventemitter3": "^5.0.1",
"lodash.clonedeep": "^4.5.0",
"lru-cache": "^10.2.0",
"promise-coalesce": "^1.1.2"
}
},
"node_modules/cache-manager/node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
},
"node_modules/cache-manager/node_modules/lru-cache": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
"integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
"engines": {
"node": "14 || >=16.14"
}
},
"node_modules/call-bind": { "node_modules/call-bind": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
@ -6569,6 +6544,14 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/character-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",
"integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==",
"dependencies": {
"is-regex": "^1.0.3"
}
},
"node_modules/chardet": { "node_modules/chardet": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
@ -6889,6 +6872,15 @@
"upper-case": "^2.0.2" "upper-case": "^2.0.2"
} }
}, },
"node_modules/constantinople": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz",
"integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==",
"dependencies": {
"@babel/parser": "^7.6.0",
"@babel/types": "^7.6.1"
}
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@ -7253,6 +7245,11 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/doctypes": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
"integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ=="
},
"node_modules/dot-case": { "node_modules/dot-case": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
@ -8638,6 +8635,20 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-unicode": { "node_modules/has-unicode": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@ -8954,7 +8965,6 @@
"version": "2.13.1", "version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
"dev": true,
"dependencies": { "dependencies": {
"hasown": "^2.0.0" "hasown": "^2.0.0"
}, },
@ -8962,6 +8972,26 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-expression": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz",
"integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==",
"dependencies": {
"acorn": "^7.1.1",
"object-assign": "^4.1.1"
}
},
"node_modules/is-expression/node_modules/acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -9033,6 +9063,26 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-promise": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="
},
"node_modules/is-regex": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
"dependencies": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-relative": { "node_modules/is-relative": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
@ -9925,6 +9975,11 @@
"url": "https://github.com/sponsors/panva" "url": "https://github.com/sponsors/panva"
} }
}, },
"node_modules/js-stringify": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
"integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g=="
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -10055,6 +10110,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/jstransformer": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz",
"integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==",
"dependencies": {
"is-promise": "^2.0.0",
"promise": "^7.0.1"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -10174,11 +10238,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
},
"node_modules/lodash.memoize": { "node_modules/lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@ -10988,8 +11047,7 @@
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
"dev": true
}, },
"node_modules/path-root": { "node_modules/path-root": {
"version": "0.1.1", "version": "0.1.1",
@ -11238,19 +11296,10 @@
"version": "7.3.1", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"dev": true,
"dependencies": { "dependencies": {
"asap": "~2.0.3" "asap": "~2.0.3"
} }
}, },
"node_modules/promise-coalesce": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz",
"integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==",
"engines": {
"node": ">=16"
}
},
"node_modules/prompts": { "node_modules/prompts": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@ -11276,6 +11325,118 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/pug": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pug/-/pug-3.0.2.tgz",
"integrity": "sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==",
"dependencies": {
"pug-code-gen": "^3.0.2",
"pug-filters": "^4.0.0",
"pug-lexer": "^5.0.1",
"pug-linker": "^4.0.0",
"pug-load": "^3.0.0",
"pug-parser": "^6.0.0",
"pug-runtime": "^3.0.1",
"pug-strip-comments": "^2.0.0"
}
},
"node_modules/pug-attrs": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz",
"integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==",
"dependencies": {
"constantinople": "^4.0.1",
"js-stringify": "^1.0.2",
"pug-runtime": "^3.0.0"
}
},
"node_modules/pug-code-gen": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.2.tgz",
"integrity": "sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==",
"dependencies": {
"constantinople": "^4.0.1",
"doctypes": "^1.1.0",
"js-stringify": "^1.0.2",
"pug-attrs": "^3.0.0",
"pug-error": "^2.0.0",
"pug-runtime": "^3.0.0",
"void-elements": "^3.1.0",
"with": "^7.0.0"
}
},
"node_modules/pug-error": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.0.0.tgz",
"integrity": "sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ=="
},
"node_modules/pug-filters": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz",
"integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==",
"dependencies": {
"constantinople": "^4.0.1",
"jstransformer": "1.0.0",
"pug-error": "^2.0.0",
"pug-walk": "^2.0.0",
"resolve": "^1.15.1"
}
},
"node_modules/pug-lexer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz",
"integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==",
"dependencies": {
"character-parser": "^2.2.0",
"is-expression": "^4.0.0",
"pug-error": "^2.0.0"
}
},
"node_modules/pug-linker": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz",
"integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==",
"dependencies": {
"pug-error": "^2.0.0",
"pug-walk": "^2.0.0"
}
},
"node_modules/pug-load": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz",
"integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==",
"dependencies": {
"object-assign": "^4.1.1",
"pug-walk": "^2.0.0"
}
},
"node_modules/pug-parser": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz",
"integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==",
"dependencies": {
"pug-error": "^2.0.0",
"token-stream": "1.0.0"
}
},
"node_modules/pug-runtime": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz",
"integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg=="
},
"node_modules/pug-strip-comments": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz",
"integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==",
"dependencies": {
"pug-error": "^2.0.0"
}
},
"node_modules/pug-walk": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz",
"integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ=="
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -11522,7 +11683,6 @@
"version": "1.22.8", "version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"dev": true,
"dependencies": { "dependencies": {
"is-core-module": "^2.13.0", "is-core-module": "^2.13.0",
"path-parse": "^1.0.7", "path-parse": "^1.0.7",
@ -12405,7 +12565,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@ -12682,7 +12841,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"dev": true,
"engines": { "engines": {
"node": ">=4" "node": ">=4"
} }
@ -12706,6 +12864,11 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/token-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
"integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg=="
},
"node_modules/tr46": { "node_modules/tr46": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@ -13162,6 +13325,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/walker": { "node_modules/walker": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@ -13356,6 +13527,20 @@
"string-width": "^1.0.2 || 2 || 3 || 4" "string-width": "^1.0.2 || 2 || 3 || 4"
} }
}, },
"node_modules/with": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz",
"integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==",
"dependencies": {
"@babel/parser": "^7.9.6",
"@babel/types": "^7.9.6",
"assert-never": "^1.2.1",
"babel-walk": "3.0.0-canary-5"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",

View File

@ -25,16 +25,15 @@
"dependencies": { "dependencies": {
"@apollo/server": "^4.10.2", "@apollo/server": "^4.10.2",
"@nestjs/apollo": "^12.1.0", "@nestjs/apollo": "^12.1.0",
"@nestjs/cache-manager": "^2.2.2",
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
"@nestjs/graphql": "^12.1.1", "@nestjs/graphql": "^12.1.1",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"@prisma/client": "^5.12.1", "@prisma/client": "^5.12.1",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cache-manager": "^5.5.1",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"joi": "17.6.4", "joi": "17.6.4",
"pug": "^3.0.2",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1" "rxjs": "^7.8.1"
}, },

View File

@ -0,0 +1,205 @@
-- CreateTable
CREATE TABLE "SystemSetting" (
"hashKey" TEXT NOT NULL PRIMARY KEY,
"hashValueType" TEXT NOT NULL,
"hashValue" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "SystemPostMigration" (
"name" TEXT NOT NULL PRIMARY KEY,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "AuthRealm" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "AuthOauth2Client" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"realmId" INTEGER NOT NULL,
"clientId" TEXT NOT NULL,
"clientSecret" TEXT,
"consentRequired" BOOLEAN NOT NULL DEFAULT false,
"authorizationCodeFlowEnabled" BOOLEAN NOT NULL DEFAULT false,
"resourceOwnerPasswordCredentialsFlowEnabled" BOOLEAN NOT NULL DEFAULT false,
"clientCredentialsFlowEnabled" BOOLEAN NOT NULL DEFAULT false,
"idTokenEnabled" BOOLEAN NOT NULL DEFAULT false,
"refreshTokenEnabled" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "AuthOauth2Client_realmId_fkey" FOREIGN KEY ("realmId") REFERENCES "AuthRealm" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "AuthOauth2Scope" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"realmId" INTEGER NOT NULL,
"scope" TEXT NOT NULL
);
-- CreateTable
CREATE TABLE "AuthOauth2ClientToAuthOauth2Scope" (
"clientId" INTEGER NOT NULL,
"scopeId" INTEGER NOT NULL,
PRIMARY KEY ("clientId", "scopeId"),
CONSTRAINT "AuthOauth2ClientToAuthOauth2Scope_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "AuthOauth2Client" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "AuthOauth2ClientToAuthOauth2Scope_scopeId_fkey" FOREIGN KEY ("scopeId") REFERENCES "AuthOauth2Scope" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "AuthOauth2ScopeToIdentityProfileAttributeName" (
"scopeId" INTEGER NOT NULL,
"claimName" TEXT NOT NULL,
"attributeId" INTEGER NOT NULL,
PRIMARY KEY ("scopeId", "attributeId"),
CONSTRAINT "AuthOauth2ScopeToIdentityProfileAttributeName_scopeId_fkey" FOREIGN KEY ("scopeId") REFERENCES "AuthOauth2Scope" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "AuthOauth2ScopeToIdentityProfileAttributeName_attributeId_fkey" FOREIGN KEY ("attributeId") REFERENCES "IdentityProfileAttributeName" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "AuthRole" (
"realmId" INTEGER NOT NULL,
"roleName" TEXT NOT NULL,
PRIMARY KEY ("realmId", "roleName"),
CONSTRAINT "AuthRole_realmId_fkey" FOREIGN KEY ("realmId") REFERENCES "AuthRealm" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "AuthAccessAttempt" (
"id" TEXT NOT NULL PRIMARY KEY,
"username" TEXT NOT NULL,
"ip" TEXT NOT NULL,
"userAgent" TEXT NOT NULL,
"requestPath" TEXT NOT NULL,
"valid" BOOLEAN NOT NULL,
"attemptedOn" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "EnumIdentityGroupRole" (
"enumValue" TEXT NOT NULL PRIMARY KEY
);
-- CreateTable
CREATE TABLE "IdentityGroup" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"realmId" INTEGER NOT NULL,
"role" TEXT NOT NULL,
"name" TEXT,
CONSTRAINT "IdentityGroup_realmId_fkey" FOREIGN KEY ("realmId") REFERENCES "AuthRealm" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "IdentityGroup_role_fkey" FOREIGN KEY ("role") REFERENCES "EnumIdentityGroupRole" ("enumValue") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "IdentityGroupToIdentityUser" (
"groupId" INTEGER NOT NULL,
"userId" INTEGER NOT NULL,
"userIsGroupAdmin" BOOLEAN NOT NULL DEFAULT false,
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,
"realmId" INTEGER NOT NULL,
CONSTRAINT "IdentityUser_realmId_fkey" FOREIGN KEY ("realmId") REFERENCES "AuthRealm" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "IdentityProfileAttributeName" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"realmId" INTEGER NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "IdentityProfileAttributeName_realmId_fkey" FOREIGN KEY ("realmId") REFERENCES "AuthRealm" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "IdentityProfileNonNormalized" (
"userId" INTEGER NOT NULL,
"attributeNameId" INTEGER NOT NULL,
"attributeValue" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ("userId", "attributeNameId"),
CONSTRAINT "IdentityProfileNonNormalized_userId_fkey" FOREIGN KEY ("userId") REFERENCES "IdentityUser" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "IdentityProfileNonNormalized_attributeNameId_fkey" FOREIGN KEY ("attributeNameId") REFERENCES "IdentityProfileAttributeName" ("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" TEXT NOT NULL PRIMARY KEY,
"userId" INTEGER NOT NULL,
"deviceType" TEXT NOT NULL,
"attributes" TEXT NOT NULL,
"preferred" BOOLEAN 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 "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,
"attributes" 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
);
-- CreateIndex
CREATE UNIQUE INDEX "AuthRealm_name_key" ON "AuthRealm"("name");
-- CreateIndex
CREATE UNIQUE INDEX "AuthOauth2Client_realmId_clientId_key" ON "AuthOauth2Client"("realmId", "clientId");
-- CreateIndex
CREATE UNIQUE INDEX "AuthOauth2Scope_realmId_scope_key" ON "AuthOauth2Scope"("realmId", "scope");
-- CreateIndex
CREATE UNIQUE INDEX "AuthOauth2ScopeToIdentityProfileAttributeName_scopeId_claimName_key" ON "AuthOauth2ScopeToIdentityProfileAttributeName"("scopeId", "claimName");
-- 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");

View File

@ -4,7 +4,7 @@ generator client {
datasource db { datasource db {
provider = "sqlite" provider = "sqlite"
url = "file:../../../data/core.db" url = "file:../../data/core.db"
} }
// //
@ -12,6 +12,7 @@ datasource db {
// //
model SystemSetting { model SystemSetting {
hashKey String @id hashKey String @id
hashValueType String
hashValue String hashValue String
} }
@ -30,6 +31,7 @@ model AuthRealm {
oauth2Clients AuthOauth2Client[] oauth2Clients AuthOauth2Client[]
groups IdentityGroup[] groups IdentityGroup[]
users IdentityUser[]
profileAttributeNames IdentityProfileAttributeName[] profileAttributeNames IdentityProfileAttributeName[]
roles AuthRole[] roles AuthRole[]
} }
@ -43,6 +45,7 @@ model AuthOauth2Client {
clientId String clientId String
clientSecret String? clientSecret String?
consentRequired Boolean @default(false)
authorizationCodeFlowEnabled Boolean @default(false) authorizationCodeFlowEnabled Boolean @default(false)
resourceOwnerPasswordCredentialsFlowEnabled Boolean @default(false) resourceOwnerPasswordCredentialsFlowEnabled Boolean @default(false)
clientCredentialsFlowEnabled Boolean @default(false) clientCredentialsFlowEnabled Boolean @default(false)
@ -97,6 +100,16 @@ model AuthRole {
@@id([realmId, roleName]) @@id([realmId, roleName])
} }
model AuthAccessAttempt {
id String @id @default(uuid())
username String
ip String
userAgent String
requestPath String
valid Boolean
attemptedOn DateTime @default(now())
}
// //
// Namespace: Identity // Namespace: Identity
// //
@ -138,6 +151,9 @@ model IdentityUser {
externalId String @unique @default(uuid()) externalId String @unique @default(uuid())
username String @unique username String @unique
realmId Int
realm AuthRealm @relation(fields: [realmId], references: [id])
groups IdentityGroupToIdentityUser[] groups IdentityGroupToIdentityUser[]
profileHashMapPairs IdentityProfileNonNormalized[] profileHashMapPairs IdentityProfileNonNormalized[]
emails IdentityUserEmails[] emails IdentityUserEmails[]
@ -163,7 +179,7 @@ model IdentityProfileNonNormalized {
attributeNameId Int attributeNameId Int
attributeName IdentityProfileAttributeName @relation(fields: [attributeNameId], references: [id]) attributeName IdentityProfileAttributeName @relation(fields: [attributeNameId], references: [id])
hashValue String attributeValue String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@id([userId, attributeNameId]) @@id([userId, attributeNameId])
@ -186,7 +202,7 @@ model EnumIdentityAuthDeviceType {
} }
model IdentityAuthDevice { model IdentityAuthDevice {
id Int @id @default(autoincrement()) id String @id @default(uuid())
userId Int userId Int
user IdentityUser @relation(fields: [userId], references: [id]) user IdentityUser @relation(fields: [userId], references: [id])
@ -194,25 +210,14 @@ model IdentityAuthDevice {
deviceType String deviceType String
deviceTypeRelation EnumIdentityAuthDeviceType @relation(fields: [deviceType], references: [enumValue]) deviceTypeRelation EnumIdentityAuthDeviceType @relation(fields: [deviceType], references: [enumValue])
attributes String
preferred Boolean
createdAt DateTime @default(now()) createdAt DateTime @default(now())
hashMapPairs IdentityAuthDeviceNonNormalized[]
@@index([userId]) @@index([userId])
@@index([userId, deviceType]) @@index([userId, deviceType])
} }
model IdentityAuthDeviceNonNormalized {
authDeviceId Int
authDevice IdentityAuthDevice @relation(fields: [authDeviceId], references: [id])
hashKey String
hashValue String
createdAt DateTime @default(now())
@@id([authDeviceId, hashKey])
}
// //
// Namespace: cloud-dav // Namespace: cloud-dav
// //
@ -231,18 +236,7 @@ model CloudDavResource {
resourceType String resourceType String
resourceTypeRelation EnumCloudDavResourceType @relation(fields: [resourceType], references: [enumValue]) resourceTypeRelation EnumCloudDavResourceType @relation(fields: [resourceType], references: [enumValue])
hashMapPairs CloudDavResourceNonNormalized[] attributes String
@@index([identityGroupId]) @@index([identityGroupId])
} }
model CloudDavResourceNonNormalized {
davResourceId String
davResource CloudDavResource @relation(fields: [davResourceId], references: [id])
hashKey String
hashValue String
createdAt DateTime @default(now())
@@id([davResourceId, hashKey])
}

View File

@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { CacheModule } from './cache/cache.module';
import { ConfigModule } from './config/config.module';
import { PersistenceModule } from './persistence/persistence.module';
import { GraphqlServerModule } from './graphql-server/graphql-server.module';
import { HttpModule } from './http/http.module';
@Module({
imports: [
HttpModule,
CacheModule,
ConfigModule,
GraphqlServerModule,
PersistenceModule,
],
controllers: [],
providers: [],
})
export class AppModule {}

View File

@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { CacheDriver } from './cache.symbols';
import { InMemoryDriver } from './in-memory.driver';
import { CacheService } from './cache.service';
@Module({
providers: [
{
provide: CacheDriver,
useValue: new InMemoryDriver(),
},
CacheService,
],
exports: [CacheService],
})
export class CacheModule {}

View File

@ -0,0 +1,32 @@
import { Inject, Injectable } from '@nestjs/common';
import { CacheDriver } from './cache.symbols';
@Injectable()
export class CacheService {
constructor(
@Inject(CacheDriver)
private readonly cacheDriver: CacheDriver,
) {}
async get(key: string): Promise<string | null> {
return this.cacheDriver.get(key);
}
async set(key: string, val: string, ttl?: number): Promise<void> {
await this.cacheDriver.set(key, val);
if (ttl) {
await this.cacheDriver.expire(key, ttl);
}
}
async exists(key: string): Promise<boolean> {
return await this.cacheDriver.exists(key) > 0;
}
async del(key: string): Promise<void> {
await this.cacheDriver.del(key);
}
}

View File

@ -0,0 +1,8 @@
export interface CacheDriver {
get(key: string): Promise<string>;
set(key: string, val: string): Promise<'OK'>;
exists(...keys: string[]): Promise<number>;
expire(key: string, seconds: number): Promise<number>;
del(key: string): Promise<number>;
}
export const CacheDriver = Symbol.for('CACHE_DRIVER');

View File

@ -0,0 +1,42 @@
import { DateUtil } from '../utils';
import { CacheDriver } from './cache.symbols';
export class InMemoryDriver implements CacheDriver {
private readonly cache: Record<string, { val: string, ttl?: DateUtil }> = {};
get(key: string): Promise<string> {
if (!this.cache[key] || !this.cache[key].ttl?.isInTheFuture()) {
return Promise.resolve(null);
}
return Promise.resolve(this.cache[key].val);
}
set(key: string, val: string): Promise<'OK'> {
this.cache[key] = { val };
return Promise.resolve('OK');
}
exists(...keys: string[]): Promise<number> {
return Promise.resolve(
keys.reduce((acc, k) => {
if (k in this.cache) {
return acc + 1;
}
return acc;
}, 0)
);
}
expire(key: string, seconds: number): Promise<number> {
const dateUtil = DateUtil.fromDate(new Date()).addNSeconds(seconds);
this.cache[key].ttl = dateUtil;
return Promise.resolve(1);
}
del(key: string): Promise<number> {
delete this.cache[key];
return Promise.resolve(1);
}
}

View File

@ -0,0 +1,36 @@
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
import { PersistenceModule } from '../persistence/persistence.module';
import { SystemSettingsDao } from '../persistence/system-settings.dao';
import { LoadedConfig } from './config.symbol';
@Module({
imports: [
PersistenceModule,
],
providers: [
// {
// provide: LoadedConfig,
// useFactory: async () => {
// const prismaService = new PrismaService();
// await prismaService.onModuleInit();
// const sysSettingsService = new SystemSettingsDao(prismaService);
// const { valueMap } = await sysSettingsService.getSettings();
// await prismaService.onModuleDestroy();
// return valueMap;
// },
// },
{
provide: LoadedConfig,
useFactory: async (sysSettingsService: SystemSettingsDao) => {
const { valueMap } = await sysSettingsService.getSettings();
return valueMap;
},
inject: [SystemSettingsDao],
},
ConfigService
],
exports: [ConfigService],
})
export class ConfigModule {}

View File

@ -0,0 +1,28 @@
import { Global, Inject, Injectable } from '@nestjs/common';
import { configValidator} from './config.validator';
import { SystemSettings } from '../domain/system-settings.types';
import { LoadedConfig } from './config.symbol';
@Injectable()
export class ConfigService {
private readonly _config: SystemSettings.Config;
constructor(
@Inject(LoadedConfig)
loadedConfig: LoadedConfig,
) {
const { value: config, error } = configValidator.validate(loadedConfig, { abortEarly: false, allowUnknown: false });
if (error) {
throw error;
}
this._config = config;
}
get<K extends keyof SystemSettings.Config>(key: K): SystemSettings.Config[K] {
return this._config[key];
}
}

View File

@ -0,0 +1,2 @@
export type LoadedConfig = Record<string, string>;
export const LoadedConfig = Symbol.for('LOADED_CONFIG');

View File

@ -0,0 +1,15 @@
import * as Joi from 'joi';
import { SystemSettings } from '../domain/system-settings.types';
export const configValidator: Joi.ObjectSchema<SystemSettings.Config> = Joi.object<SystemSettings.Config, true>({
[SystemSettings.Auth.AccessAttempts.CheckForwardedFor]: Joi.boolean().required(),
[SystemSettings.Auth.AccessAttempts.MaxAttempts]: Joi.number().required(),
[SystemSettings.Auth.AccessAttempts.Timeout]: Joi.number().required(),
[SystemSettings.Auth.Oauth2.Enabled]: Joi.boolean().required(),
[SystemSettings.Auth.Oauth2.EncryptionSecret]: Joi.string().required(),
[SystemSettings.Auth.TokenManagement.SigningSecret]: Joi.string().required(),
[SystemSettings.Graphql.Debug]: Joi.boolean().required(),
[SystemSettings.Graphql.IntrospectionEnabled]: Joi.boolean().required(),
[SystemSettings.Graphql.PlaygroundEnabled]: Joi.boolean().required(),
});

View File

@ -1,4 +1,14 @@
export namespace Auth { export namespace Authentication {
export namespace AccessAttempt {
export interface CreateDto {
username: string;
ip: string;
userAgent: string;
requestPath: string;
valid: boolean;
}
}
export namespace Login { export namespace Login {
export enum ResponseStatus { export enum ResponseStatus {
@ -8,6 +18,20 @@ export namespace Auth {
Timedout = 'timedout', Timedout = 'timedout',
DeviceLocked = 'locked', DeviceLocked = 'locked',
} }
export interface State {
jwi: string;
exp: number;
tfa: boolean;
}
export interface RequestContext {
isComplete: boolean;
rawState?: Object;
state?: State;
username?: string;
isLocked?: boolean;
}
} }
export namespace Oauth2 { export namespace Oauth2 {

View File

@ -0,0 +1,39 @@
export namespace Identity {
export interface AuthDevice {
readonly urn: string,
readonly userId: number,
readonly deviceType: AuthDevice.Type,
readonly preferred: boolean;
readonly createdAt: Date,
}
export namespace AuthDevice {
export enum Type {
Password = 'password',
ApplicationPassword = 'application_password',
}
export enum PasswordHashKey {
Expiry = 'expiry',
PasswordHashString = 'password_hash_string',
Locked = 'locked',
}
export interface PasswordAttributes {
readonly expiry: Date;
readonly passwordHashString: string;
readonly locked: boolean;
}
export type Password = AuthDevice | PasswordAttributes;
}
export namespace Group {
export enum Role {
SystemAdmin = 'SYSTEM_ADMIN',
RealmAdmin = 'REALM_ADMIN',
Standard = 'STANDARD',
}
}
}

View File

@ -0,0 +1,18 @@
export abstract class Serializable {
toJson(): string {
return JSON.stringify(this);
}
static fromJson(): ThisType<Serializable> {
throw new Error('Method not implemented! Use derived class');
}
protected static safeJsonParse(str: string): [Error, null] | [null, Object] {
try {
return [null, JSON.parse(str)];
} catch (err) {
return [err, null];
}
}
}

View File

@ -0,0 +1,43 @@
export namespace SystemSettings {
export interface Config {
[Auth.AccessAttempts.CheckForwardedFor]: boolean;
[Auth.AccessAttempts.MaxAttempts]: number;
[Auth.AccessAttempts.Timeout]: number;
[Auth.Oauth2.Enabled]: boolean;
[Auth.Oauth2.EncryptionSecret]: string;
[Auth.TokenManagement.SigningSecret]: string;
[Graphql.Debug]: boolean;
[Graphql.IntrospectionEnabled]: boolean;
[Graphql.PlaygroundEnabled]: boolean;
}
export namespace Auth {
export enum AccessAttempts {
MaxAttempts = 'auth.attempts.max',
Timeout = 'auth.attempts.timeout-ms',
CheckForwardedFor = 'auth.attempts.check-forwarded-for-header'
}
export enum Oauth2 {
Enabled = 'auth.oauth2.enabled',
EncryptionSecret = 'auth.oauth2.encryption_secret',
}
export enum TokenManagement {
SigningSecret = 'auth.token-management.signing_secret',
}
}
export enum Graphql {
Debug = 'graphql.debug.enabled',
IntrospectionEnabled = 'graphql.introspection.enabled',
PlaygroundEnabled = 'graphql.playground.enabled',
}
export namespace Dav {
export enum Contacts {
Enabled = 'dav.contacts.enabled',
}
}
}

View File

@ -3,22 +3,22 @@ import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql'; import { GraphQLModule } from '@nestjs/graphql';
import { ConfigService } from '../config/config.service'; import { ConfigService } from '../config/config.service';
import { SystemSettings } from '../enumerations';
import { SystemSettingsResolver } from './system-settings.resolver'; import { SystemSettingsResolver } from './system-settings.resolver';
import { PrismaModule } from '../prisma/prisma.module';
import { ConfigModule } from '../config/config.module'; import { ConfigModule } from '../config/config.module';
import { SystemSettings } from '../domain/system-settings.types';
import { PersistenceModule } from '../persistence/persistence.module';
@Module({ @Module({
imports: [ imports: [
ConfigModule, ConfigModule,
PrismaModule, PersistenceModule,
GraphQLModule.forRootAsync<ApolloDriverConfig>({ GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver, driver: ApolloDriver,
useFactory: async (configService: ConfigService) => ({ useFactory: async (configService: ConfigService) => ({
debug: await configService.get(SystemSettings.Graphql.Debug), debug: await configService.get(SystemSettings.Graphql.Debug),
playground: await configService.get(SystemSettings.Graphql.PlaygroundEnabled), playground: await configService.get(SystemSettings.Graphql.PlaygroundEnabled),
introspection: await configService.get(SystemSettings.Graphql.IntrospectionEnabled), introspection: await configService.get(SystemSettings.Graphql.IntrospectionEnabled),
typePaths: ['../../graphql/**/*.graphql'], typePaths: ['../graphql/**/*.graphql'],
}), }),
inject: [ConfigService], inject: [ConfigService],
imports: [ConfigModule], imports: [ConfigModule],
@ -28,4 +28,4 @@ import { ConfigModule } from '../config/config.module';
SystemSettingsResolver, SystemSettingsResolver,
] ]
}) })
export class GraphqlModule {} export class GraphqlServerModule {}

View File

@ -0,0 +1,79 @@
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Logger } from '@nestjs/common';
import { SystemSettingsQueryOutput, SystemSettingsQueryOutputError, UpdateSystemSettingInput, UpdateSystemSettingOutput, UpdateSystemSettingOutputError, SystemSettingHashValueTypeEnum } from '../.generated/graphql';
import { SystemSettingsDao } from '../persistence/system-settings.dao';
const getUrn = (hashKey: string): string => `urn:system-settings:${hashKey}`;
const parseUrn = (urn: string): string => urn.split(':').pop();
const t2g: Record<'string' | 'boolean' | 'number', SystemSettingHashValueTypeEnum> = {
string: SystemSettingHashValueTypeEnum.String,
boolean: SystemSettingHashValueTypeEnum.Boolean,
number: SystemSettingHashValueTypeEnum.Number,
}
const g2t: Record<SystemSettingHashValueTypeEnum, 'string' | 'boolean' | 'number'> = {
[SystemSettingHashValueTypeEnum.String]: 'string',
[SystemSettingHashValueTypeEnum.Boolean]: 'boolean',
[SystemSettingHashValueTypeEnum.Number]: 'number',
}
@Resolver('SystemSettings')
export class SystemSettingsResolver {
private readonly logger: Logger = new Logger(SystemSettingsResolver.name);
constructor(
private readonly systemSettingsDao: SystemSettingsDao,
) {}
@Query()
async systemSettings(): Promise<SystemSettingsQueryOutput> {
try {
const { valueMap, typeMap } = await this.systemSettingsDao.getSettings();
return {
data: Object.entries(valueMap).map(([hashKey, hashValue]) => ({
urn: getUrn(hashKey),
hashKey,
hashValue: hashValue.toString(),
hashValueType: t2g[typeMap[hashKey]],
})),
}
} catch (e) {
this.logger.error(e.message);
return {
error: SystemSettingsQueryOutputError.Unknown,
}
}
}
@Mutation()
async updateSystemSetting(
@Args('input') { urn, hashValue, hashValueType }: UpdateSystemSettingInput
): Promise<UpdateSystemSettingOutput> {
try {
const hashKey = parseUrn(urn);
const exists = await this.systemSettingsDao.hashKeyExists(hashKey);
if (!exists) {
return {
error: UpdateSystemSettingOutputError.NotFound,
}
}
await this.systemSettingsDao.setSettingWithType(hashKey, g2t[hashValueType], hashValue);
return {};
} catch (e) {
this.logger.error(e.message);
return {
error: UpdateSystemSettingOutputError.Unknown,
}
}
}
}

View File

@ -0,0 +1,7 @@
import { HttpException, HttpStatus } from '@nestjs/common';
export class TooManyRequestsException extends HttpException {
constructor() {
super('Too Many Requests', HttpStatus.TOO_MANY_REQUESTS);
}
}

View File

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { LoginModule } from './mvc/login/login.module';
@Module({
imports: [LoginModule],
})
export class HttpModule {}

View File

@ -0,0 +1,99 @@
import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { ConfigService } from '../../../config/config.service';
import { SystemSettings } from '../../../domain/system-settings.types';
import { AuthAccessAttemptDao } from '../../../persistence/auth-access-attempt.dao';
import { CacheService } from '../../../cache/cache.service';
export enum AccessAttemptError {
}
interface RequestData {
userAgent: string;
requestPath: string;
ip: string;
ipAddressKey: string;
usernameKey: string;
}
@Injectable()
export class AccessAttemptService {
private readonly maxAttemptsAllowed: number;
private readonly lockTimeout: number;
constructor(
private readonly cacheService: CacheService,
private readonly accessAttemptService: AuthAccessAttemptDao,
configService: ConfigService,
) {
this.maxAttemptsAllowed = configService.get(SystemSettings.Auth.AccessAttempts.MaxAttempts);
this.lockTimeout = configService.get(SystemSettings.Auth.AccessAttempts.Timeout);
}
async checkIfLocked(request: Request, username: string): Promise<boolean> {
const { ipAddressKey, usernameKey } = this.parseRequestData(request, username);
const ipAttempts = +await this.cacheService.get(ipAddressKey) ?? 0;
const usernameAttempts = +await this.cacheService.get(usernameKey) ?? 0;
if (ipAttempts >= this.maxAttemptsAllowed || usernameAttempts >= this.maxAttemptsAllowed) {
await this.cacheService.set(ipAddressKey, (ipAttempts + 1).toString(), this.lockTimeout);
await this.cacheService.set(usernameKey, (usernameAttempts + 1).toString(), this.lockTimeout);
return true;
}
return false;
}
async recordAttempt(request: Request, username: string, loginSucceeded: boolean): Promise<void> {
const isLocked = await this.checkIfLocked(request, username);
if (isLocked) {
return;
}
const {
userAgent,
requestPath,
ip,
ipAddressKey,
usernameKey,
} = this.parseRequestData(request, username);
await this.accessAttemptService.createAttempt({
userAgent,
username,
ip,
requestPath,
valid: loginSucceeded,
});
if (loginSucceeded) {
await this.cacheService.del(ipAddressKey);
await this.cacheService.del(usernameKey);
}
}
private parseRequestData(request: Request, username: string): RequestData {
const userAgent = request.headers['user-agent'];
const requestPath = request.path;
const ip = request.ip;
const ipAddressKey = `access-attempt:ip:${ip}`;
const usernameKey = `access-attempt:username:${username}`;
return {
userAgent,
requestPath,
ip,
ipAddressKey,
usernameKey,
}
}
}

View File

@ -0,0 +1,42 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import * as Joi from 'joi';
import { DateUtil } from '../../../utils';
import { Authentication } from '../../../domain/authentication.types';
export const Context = createParamDecorator(
(data: unknown, ctx: ExecutionContext): Authentication.Login.RequestContext => {
const request = ctx.switchToHttp().getRequest();
const loginContext: Authentication.Login.RequestContext = request?.context;
if (!loginContext.username) {
return loginContext;
}
const state = loginContext?.rawState;
if (!state) {
return loginContext;
}
const { error, value } = Joi.object<Authentication.Login.State, true>({
jwi: Joi.string().required(),
exp: Joi.number().required(),
tfa: Joi.boolean().required(),
}).validate(state, { allowUnknown: false });
if (error) {
return loginContext;
}
if (DateUtil.fromSecondsSinceEpoch(value?.exp).isInThePast()) {
return loginContext;
}
return {
...loginContext,
isComplete: true,
state: value,
};
},
);

View File

@ -0,0 +1,39 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { TokenManagementService } from '../../../token-management/token-management.service';
import { AccessAttemptService } from './access-attempt.service';
import { Authentication } from '../../../domain/authentication.types';
@Injectable()
export class LoginContextInterceptor implements NestInterceptor {
constructor(
private readonly tokenService: TokenManagementService,
private readonly accessAttemptService: AccessAttemptService,
) {}
async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const loginContext: Authentication.Login.RequestContext = { isComplete: false };
if (!request?.body?.state) {
return next.handle();
}
const receipt = this.tokenService.parseToken(request.body.state);
if (receipt.isToken && receipt.signatureValid) {
loginContext.rawState = receipt.payload;
}
loginContext.username = request?.body?.username;
if (loginContext.username) {
loginContext.isLocked = await this.accessAttemptService.checkIfLocked(request, loginContext.username);
}
request.context = loginContext;
return next.handle();
}
}

View File

@ -0,0 +1,130 @@
import { BadRequestException, Controller, Get, Param, Post, Query, Render, Res, UseInterceptors } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import { Authentication } from '../../../domain/authentication.types';
import { TokenManagementService } from '../../../token-management/token-management.service';
import { DateUtil } from '../../../utils';
import { TooManyRequestsException } from '../../exceptions/too-many-requests.exception';
import { AccessAttemptService } from './access-attempt.service';
import { Context } from './context.decorator';
import { LoginContextInterceptor } from './login-context.interceptor';
import { LoginStages } from './login.types';
import { IdentityAuthDeviceDao } from '../../../persistence/identity-auth-device.dao';
import { Response } from 'express';
import { CacheService } from '../../../cache/cache.service';
import { Identity } from '../../../domain/identity.types';
const getStateKey = (jwi: string) => `login:state:${jwi}`;
const challengeToTemplate = {
[Identity.AuthDevice.Type.Password]: 'login-password-challenge',
}
@Controller({
version: '1',
path: 'auth/:realm/signin',
})
@UseInterceptors(LoginContextInterceptor)
export class LoginController {
constructor(
private readonly tokenService: TokenManagementService,
private readonly cacheService: CacheService,
private readonly accessAttemptService: AccessAttemptService,
private readonly identityAuthDeviceDao: IdentityAuthDeviceDao,
) {}
@Get('identifier')
@Render('login-view')
async getLogin(
@Param('realm') realm: string,
@Query('error') error?: string,
): Promise<LoginStages.IdentifierView.RenderContext> {
const date = DateUtil.fromDate(new Date()).addNMinutes(15);
const stateObj: Authentication.Login.State = {
jwi: randomUUID(),
exp: date.seconds,
tfa: false,
}
await this.cacheService.set(getStateKey(stateObj.jwi), new Date().toISOString(), date.toDate().getTime());
return {
realm,
state: this.tokenService.createToken(stateObj),
user_settings: {
theme: 'dark',
},
links: {
identifier_form: `/auth/${realm}/signin/identifier`
}
}
}
@Post('identifier')
async postLogin(
@Param('realm') realm: string,
@Context() context: Authentication.Login.RequestContext,
@Res() res: Response,
) {
if (!context.isComplete) {
throw new BadRequestException();
}
if (context.isLocked) {
throw new TooManyRequestsException();
}
const devicesForUser = await this.identityAuthDeviceDao.findByRealmAndUsername(realm, context.username);
const selectedDevice = devicesForUser.length === 1 ? devicesForUser[0] : devicesForUser.find(d => d.preferred);
if (selectedDevice) {
const renderContext: LoginStages.ChallengeView.RenderContext = {
realm,
state: this.tokenService.createToken(context.state),
username: context.username,
user_settings: {
theme: 'dark',
},
links: {
select_device: devicesForUser.length > 1 ? `/auth/${realm}/signin/challenge` : null,
challenge_form: `/auth/${realm}/signin/challenge/${selectedDevice.urn}`,
}
}
return res.render(challengeToTemplate[selectedDevice.deviceType], renderContext);
}
}
@Get('challenge')
async selectDevice() {
}
@Post('challenge/:device_urn')
async postDevice(
@Param('realm') realm: string,
@Context() context: Authentication.Login.RequestContext,
@Res() res: Response,
) {
}
@Get('2fa/challenge')
async select2faDevice() {
}
@Post('2fa/challenge/:device_urn')
async post2faDevice() {
}
@Post('logout')
async logout() {
}
}

View File

@ -0,0 +1,26 @@
import { Module } from '@nestjs/common';
import { CacheModule } from '../../../cache/cache.module';
import { PersistenceModule } from '../../../persistence/persistence.module';
import { LoginController } from './login.controller';
import { AccessAttemptService } from './access-attempt.service';
import { LoginContextInterceptor } from './login-context.interceptor';
import { ConfigModule } from '../../../config/config.module';
import { TokenManagementModule } from '../../../token-management/token-management.module';
@Module({
imports: [
CacheModule,
PersistenceModule,
ConfigModule,
TokenManagementModule,
],
controllers: [
LoginController,
],
providers: [
AccessAttemptService,
LoginContextInterceptor,
],
})
export class LoginModule {}

View File

@ -0,0 +1,70 @@
import { Identity } from '../../../domain/identity.types';
import { Authentication } from '../../../domain/authentication.types';
import { GlobalProps } from '../mvc.types';
export namespace LoginStages {
export enum Error {
INVALID,
}
export const errorDescriptionMap: Record<Error, string> = {
[Error.INVALID]: 'Invalid username'
}
export namespace IdentifierView {
export interface RenderContext extends GlobalProps {
state: string;
realm: string;
links: {
identifier_form: string;
}
}
}
export namespace ChallengeView {
export interface RenderContext extends GlobalProps {
state: string;
realm: string;
username: string;
links: {
select_device: string | null;
challenge_form: string;
}
}
}
export namespace $7PostLoginContext_Result {
export type Context = Success | TwoFactorRequired | Failure | Timedout | DeviceLocked;
type Success = {
status: Authentication.Login.ResponseStatus.Success;
state: string;
redirectUri: string;
}
type TwoFactorRequired = {
status: Authentication.Login.ResponseStatus.TwoFactorRequired;
state: string;
availableDevices: Identity.AuthDevice.Type[];
}
type Failure = {
status: Authentication.Login.ResponseStatus.Failure;
state: string;
}
type Timedout = {
status: Authentication.Login.ResponseStatus.Timedout;
}
type DeviceLocked = {
status: Authentication.Login.ResponseStatus.DeviceLocked;
}
}
}
export const supportedDevices = [
Identity.AuthDevice.Type.Password,
]

View File

@ -0,0 +1,5 @@
export interface GlobalProps {
user_settings: {
theme: 'light' | 'dark';
}
}

View File

@ -1,15 +1,15 @@
import { Auth } from '../../enumerations/auth.enumerations'; import { Authentication } from '../../../domain/authentication.types';
interface ErrorResponse { interface ErrorResponse {
state?: string; state?: string;
error: Auth.Oauth2.Error; error: Authentication.Oauth2.Error;
error_description?: string; error_description?: string;
error_uri?: string; error_uri?: string;
} }
namespace AuthorizationCode { namespace AuthorizationCode {
interface AuthorizationRequest { interface AuthorizationRequest {
response_type: Auth.Oauth2.ResponseType.Code; response_type: Authentication.Oauth2.ResponseType.Code;
client_id: string; client_id: string;
redirect_uri?: string; redirect_uri?: string;
scope?: string; scope?: string;
@ -22,7 +22,7 @@ namespace AuthorizationCode {
} }
interface AccessTokenRequest { interface AccessTokenRequest {
grant_type: Auth.Oauth2.AuthorizationGrant.AuthorizationCode; grant_type: Authentication.Oauth2.AuthorizationGrant.AuthorizationCode;
code: string; code: string;
redirect_uri?: string; redirect_uri?: string;
client_id: string; client_id: string;
@ -41,7 +41,7 @@ namespace AuthorizationCode {
// Basic base64(clientId:clientSecret) // Basic base64(clientId:clientSecret)
namespace ResourceOwner { namespace ResourceOwner {
interface AccessTokenRequest { interface AccessTokenRequest {
grant_type: Auth.Oauth2.GrantType.Password; grant_type: Authentication.Oauth2.GrantType.Password;
username: string; username: string;
password: string; password: string;
scope?: string; scope?: string;
@ -58,7 +58,7 @@ namespace ResourceOwner {
// `confidential` only // `confidential` only
namespace ClientCredentials { namespace ClientCredentials {
interface AccessTokenRequest { interface AccessTokenRequest {
// grant_type: Auth.Oauth2.GrantType.ClientCredentials; // grant_type: Authentication.Oauth2.GrantType.ClientCredentials;
scope?: string; scope?: string;
} }

View File

@ -0,0 +1,28 @@
import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
import { AppModule } from './app.module';
import { ConfigService } from './config/config.service';
import { SystemSettings } from './domain/system-settings.types';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
const configService: ConfigService = app.get(ConfigService);
const logger: Logger = new Logger();
app.useLogger(logger);
app.set('view engine', 'pug');
app.setBaseViewsDir(join(__dirname, '../../views'));
if (configService.get(SystemSettings.Auth.AccessAttempts.CheckForwardedFor)) {
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
}
await app.listen(3000, () => {
logger.log('Listening on port 3000', 'main.ts');
});
}
bootstrap();

View File

@ -0,0 +1,18 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { Authentication } from '../domain/authentication.types';
@Injectable()
export class AuthAccessAttemptDao {
constructor(
private readonly prismaService: PrismaService,
) {}
async createAttempt(data: Authentication.AccessAttempt.CreateDto): Promise<void> {
await this.prismaService.authAccessAttempt.create({
data,
});
}
}

View File

@ -0,0 +1,81 @@
import * as crypto from 'node:crypto';
import { SecureStringUtil } from '../../utils';
import { PrismaService } from '../../persistence/prisma.service';
import { DataMigration } from './data-migration.interface';
import { Identity } from '../../domain/identity.types';
import { SystemSettings } from '../../domain/system-settings.types';
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) => {
const realm = await tx.authRealm.create({
data: {
name: 'main',
}
});
const adminGroup = await tx.identityGroup.create({
data: {
role: Identity.Group.Role.RealmAdmin,
realmId: realm.id,
}
});
const adminUser = await tx.identityUser.create({
data: {
username: 'admin',
realmId: realm.id,
}
});
await tx.identityGroupToIdentityUser.create({
data: {
userId: adminUser.id,
groupId: adminGroup.id,
},
});
const passwordDevice: Identity.AuthDevice.PasswordAttributes = {
expiry: new Date(0),
passwordHashString: await SecureStringUtil.generateNewHash('ThisIsExpired123!'),
locked: false,
}
await tx.identityAuthDevice.create({
data: {
userId: adminUser.id,
deviceType: Identity.AuthDevice.Type.Password,
attributes: JSON.stringify(passwordDevice),
preferred: true,
}
});
const startingConfig: SystemSettings.Config = {
[SystemSettings.Auth.AccessAttempts.CheckForwardedFor]: true,
[SystemSettings.Auth.AccessAttempts.MaxAttempts]: 5,
[SystemSettings.Auth.AccessAttempts.Timeout]: 15 * 60 * 1000,
[SystemSettings.Auth.Oauth2.Enabled]: true,
[SystemSettings.Auth.Oauth2.EncryptionSecret]: crypto.randomBytes(16).toString('hex'),
[SystemSettings.Auth.TokenManagement.SigningSecret]: crypto.randomBytes(16).toString('hex'),
[SystemSettings.Graphql.Debug]: false,
[SystemSettings.Graphql.IntrospectionEnabled]: true,
[SystemSettings.Graphql.PlaygroundEnabled]: true,
}
const paired = Object.entries(startingConfig)
.map(([hashKey, hashValue]) => ({ hashKey, hashValue: hashValue.toString(), hashValueType: typeof hashValue }));
await tx.systemSetting.createMany({
data: paired,
});
});
}
}

View File

@ -1,4 +1,4 @@
import { PrismaService } from '../prisma.service'; import { PrismaService } from '../../persistence/prisma.service';
export interface DataMigration { export interface DataMigration {
name: string; name: string;

View File

@ -0,0 +1,73 @@
import { Injectable } from '@nestjs/common';
import { IdentityAuthDevice } from '@prisma/client';
import { PrismaService } from './prisma.service';
import { Identity } from '../domain/identity.types';
import { safeJsonParse } from '../utils';
@Injectable()
export class IdentityAuthDeviceDao {
constructor(
private readonly prismaService: PrismaService,
) {}
async findByRealmAndUsername(realm: string, username: string): Promise<Identity.AuthDevice[]> {
const devices = await this.prismaService.identityAuthDevice.findMany({
where: {
user: {
username: username.toLowerCase(),
realm: {
name: realm.toLowerCase(),
},
},
},
});
return devices.map(d => IdentityAuthDeviceDao.modelToEntity(d, true));
}
async findOneByUrn(urn: string): Promise<Identity.AuthDevice> {
const device = await this.prismaService.identityAuthDevice.findFirst({
where: {
id: urn.replace('urn:identity:auth-device:', ''),
},
});
return IdentityAuthDeviceDao.modelToEntity(device);
}
private static modelToEntity(model: IdentityAuthDevice, skipAttributes = false): Identity.AuthDevice {
const [_, attributes] = safeJsonParse(model.attributes);
const entity: Identity.AuthDevice = {
urn: `urn:identity:auth-device:${model.id}`,
userId: model.userId,
deviceType: model.deviceType as Identity.AuthDevice.Type,
preferred: model.preferred,
createdAt: new Date(model.createdAt),
}
if (skipAttributes) {
return entity;
}
if (model.deviceType === Identity.AuthDevice.Type.Password) {
const passwordEntity: Identity.AuthDevice.PasswordAttributes = {
expiry: new Date(attributes[Identity.AuthDevice.PasswordHashKey.Expiry] as string),
passwordHashString: attributes[Identity.AuthDevice.PasswordHashKey.PasswordHashString] as string,
locked: attributes[Identity.AuthDevice.PasswordHashKey.Locked] as boolean,
}
return {
...entity,
...passwordEntity,
}
}
return entity;
}
}

View File

@ -0,0 +1,21 @@
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { AuthAccessAttemptDao } from './auth-access-attempt.dao';
import { IdentityAuthDeviceDao } from './identity-auth-device.dao';
import { SystemSettingsDao } from './system-settings.dao';
@Module({
providers: [
PrismaService,
AuthAccessAttemptDao,
IdentityAuthDeviceDao,
SystemSettingsDao,
],
exports: [
AuthAccessAttemptDao,
IdentityAuthDeviceDao,
SystemSettingsDao,
],
})
export class PersistenceModule {}

View File

@ -0,0 +1,13 @@
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}

View File

@ -0,0 +1,61 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import { SystemSettings } from '../domain/system-settings.types';
type Result = {
typeMap: Record<string, 'string' | 'boolean' | 'number'>;
valueMap: Record<string, string>;
}
@Injectable()
export class SystemSettingsDao {
constructor(
private readonly prismaService: PrismaService,
) {}
async getSettings(): Promise<Result> {
const settings = await this.prismaService.systemSetting.findMany();
const typeMap = {}
const valueMap = {}
for (const { hashKey, hashValue, hashValueType } of settings) {
typeMap[hashKey] = hashValueType;
valueMap[hashKey] = hashValue;
}
return {
valueMap,
typeMap,
}
}
async hashKeyExists(hashKey: string): Promise<boolean> {
return await this.prismaService.systemSetting.count({
where: {
hashKey
}
}) > 0;
}
async setSetting(hashKey: keyof SystemSettings.Config, hashValue: string | boolean | number): Promise<void> {
await this.prismaService.systemSetting.update({
where: { hashKey },
data: {
hashValue: hashValue.toString(),
hashValueType: typeof hashValue
},
});
}
async setSettingWithType(hashKey: string, hashValueType: 'string' | 'boolean' | 'number', hashValue: string): Promise<void> {
await this.prismaService.systemSetting.update({
where: { hashKey },
data: {
hashValue: hashValue,
hashValueType: hashValueType,
},
});
}
}

View File

@ -0,0 +1,53 @@
import { NestFactory, repl } from '@nestjs/core';
import { PersistenceModule } from './persistence/persistence.module';
import { PrismaService } from './persistence/prisma.service';
import { DataMigration } from './persistence/data-migrations/data-migration.interface';
import { _00ApplicationBootstrapDataMigration } from './persistence/data-migrations';
import { CloudDav } from './domain/cloud-dav.types';
import { Identity } from './domain/identity.types';
async function bootstrap() {
const app = await NestFactory.create(PersistenceModule);
const prismaService = app.get(PrismaService);
await ensureEnumIntegrity(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);
}
}
async function ensureEnumIntegrity(prisma: PrismaService) {
const enumsRegistered: [string, string[]][] = [
['enumIdentityGroupRole', Object.values<string>(Identity.Group.Role)],
['enumIdentityAuthDeviceType', Object.values<string>(Identity.AuthDevice.Type)],
['enumCloudDavResourceType', Object.values<string>(CloudDav.Resource.Type)],
];
for (const [dbRunner, known] of enumsRegistered) {
await prisma.$transaction(async (tx) => {
const result = await tx[dbRunner].findMany();
const existing = result.map(e => e.enumValue);
const missing = known.filter(k => !existing.includes(k));
await tx[dbRunner].createMany({
data: missing.map(enumValue => ({ enumValue })),
});
});
}
}
bootstrap();

View File

@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { CacheModule } from '../cache/cache.module';
import { TokenManagementService } from './token-management.service';
import { PersistenceModule } from '../persistence/persistence.module';
import { ConfigModule } from '../config/config.module';
@Module({
imports: [
CacheModule,
ConfigModule,
PersistenceModule,
],
providers: [TokenManagementService],
exports: [TokenManagementService],
})
export class TokenManagementModule {}

View File

@ -0,0 +1,82 @@
import { Injectable } from '@nestjs/common';
import * as Joi from 'joi';
import { ConfigService } from '../config/config.service';
import { SystemSettings } from '../domain/system-settings.types';
import { JWA, TokenSignatureUtil, safeJsonParse } from '../utils';
import { CacheService } from '../cache/cache.service';
type Receipt = {
isToken: true;
signatureValid: boolean;
payload: Object;
revoked: boolean;
} | { isToken: false };
type Header = {
typ: 'JWT';
alg: JWA.HSA256;
}
@Injectable()
export class TokenManagementService {
constructor(
private readonly configService: ConfigService,
private readonly cacheService: CacheService,
) {}
createToken(payload: Object): string {
const header: Header = {
alg: JWA.HSA256,
typ: 'JWT',
}
const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8').toString('base64url');
const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64url');
const signature = TokenSignatureUtil.generateSignature(`${encodedHeader}.${encodedPayload}`, header.alg, this.configService.get(SystemSettings.Auth.TokenManagement.SigningSecret));
return `${encodedHeader}.${encodedPayload}.${signature}`;
}
parseToken(token: string): Receipt {
const parts = token.split('.');
if (parts.length !== 3) {
return { isToken: false }
}
const [encodedHeader, encodedPayload, signature] = parts;
const headerJson = Buffer.from(encodedHeader, 'base64url').toString('utf-8');
const payloadJson = Buffer.from(encodedPayload, 'base64url').toString('utf-8');
const [err1, headerObj] = safeJsonParse(headerJson);
const [err2, payloadObj] = safeJsonParse(payloadJson);
if (err1 || err2) {
return { isToken: false }
}
const { error: err3, value: header } = Joi.object<Header, true>({
alg: Joi.string().required().valid(JWA.HSA256),
typ: Joi.string().required().allow('JWT'),
}).validate(headerObj, { allowUnknown: false });
if (err3) {
return { isToken: false }
}
const regeneratedSignature = TokenSignatureUtil.generateSignature(`${encodedHeader}.${encodedPayload}`, header.alg, this.configService.get(SystemSettings.Auth.TokenManagement.SigningSecret));
const signatureValid = regeneratedSignature === signature;
return {
isToken: true,
signatureValid,
payload: payloadObj,
revoked: false,
}
}
}

View File

@ -0,0 +1,91 @@
export class DateUtil {
private readonly original: Date | null;
constructor(
private _date: Date | null,
) {
this.original = DateUtil.newDate(_date);
}
static fromIso(iso: string) {
return new DateUtil(DateUtil.newDate(iso));
}
static fromDate(date: Date) {
return new DateUtil(date);
}
static fromSecondsSinceEpoch(seconds: number) {
return new DateUtil(new Date(seconds * 1000));
}
get seconds() {
return Math.floor(this._date.getTime() / 1000);
}
isInThePast(onNull = false): boolean {
if (!this._date) {
return onNull;
}
return this._date < new Date();
}
isInTheFuture(onNull = false): boolean {
if (!this._date) {
return onNull;
}
return this._date > new Date();
}
removeNDays(n: number): DateUtil {
if (!this._date) {
return this;
}
this._date.setDate(this._date.getDate() - n);
return this;
}
addNDays(n: number): DateUtil {
if (!this._date) {
return this;
}
this._date.setDate(this._date.getDate() + n);
return this;
}
addNSeconds(n: number): DateUtil {
if (!this._date) {
return this;
}
this._date = new Date(this._date.getTime() + n * 1000);
return this;
}
addNMinutes(n: number): DateUtil {
return this.addNSeconds(n * 60);
}
reset(): DateUtil {
this._date = DateUtil.newDate(this.original);
return this;
}
toDate(): Date {
return DateUtil.newDate(this._date);
}
private static newDate(date: string | Date | null): Date | null {
return date ? new Date(date) : null;
}
}

View File

@ -1,4 +1,5 @@
export * from './normalize-hash-set'; export * from './date-util';
export * from './safe-json-parse';
export * from './secure-string'; export * from './secure-string';
export * from './snake-to-camel'; export * from './snake-to-camel';
export * from './when-clause'; export * from './when-clause';

View File

@ -0,0 +1,7 @@
export const safeJsonParse = (json: string): [Error, null] | [null, Object] => {
try {
return [null, JSON.parse(json)];
} catch (err) {
return [err, null];
}
}

View File

@ -1,4 +1,5 @@
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import * as crypto from 'node:crypto';
// https://passlib.readthedocs.io/en/stable/modular_crypt_format.html#mcf-identifiers // https://passlib.readthedocs.io/en/stable/modular_crypt_format.html#mcf-identifiers
type OSDefinedHashPrefix = | type OSDefinedHashPrefix = |
@ -57,6 +58,18 @@ const algImplementationMap: Record<NodeCryptoAlgs, HashingFunction> = {
} }
} }
export enum JWA {
HSA256 = 'HSA256',
}
type SigningFunction = (rawString: string, signingComponent: string) => string;
const jwaImplementationMap: Record<JWA, SigningFunction> = {
[JWA.HSA256]: function (rawString: string, signingComponent: string) {
return crypto.createHmac('sha-256', signingComponent).update(rawString).digest('base64url');
},
}
export class SecureStringUtil { export class SecureStringUtil {
private static readonly defaultWorkFactor = 15; private static readonly defaultWorkFactor = 15;
@ -81,6 +94,12 @@ export class SecureStringUtil {
} }
} }
export class TokenSignatureUtil {
static generateSignature(rawString: string, alg: JWA, signingComponent: string): string {
return jwaImplementationMap[alg](rawString, signingComponent);
}
}
class HashMeta { class HashMeta {
constructor( constructor(

View File

@ -9,13 +9,16 @@
"target": "ES2021", "target": "ES2021",
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
"baseUrl": "./", "baseUrl": "./src",
"incremental": true, "incremental": true,
"skipLibCheck": true, "skipLibCheck": true,
"strictNullChecks": false, "strictNullChecks": false,
"noImplicitAny": false, "noImplicitAny": false,
"strictBindCallApply": false, "strictBindCallApply": false,
"forceConsistentCasingInFileNames": false, "forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false "noFallthroughCasesInSwitch": false,
} },
"exclude": [
"codegen.ts"
]
} }

View File

@ -0,0 +1,7 @@
doctype html
html(data-theme=user_settings.theme)
head
link(rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css")
script(src="https://unpkg.com/htmx.org@1.9.12" integrity="sha384-ujb1lZYygJmzgSwoxRggbCHcjc0rB2XoQrxeTUQyRjrOnlCoYta87iKBWq3EsdM2" crossorigin="anonymous")
body
block content

View File

@ -0,0 +1,9 @@
div#innerTarget
form(hx-post=links.challenge_form hx-select="#innerTarget" hx-swap="outerHTML")
fieldset
input(type="hidden" name="state" value=state)
input(type="text" name="username" placeholder="Username" value=username readonly)
input(type="password" name="password" placeholder="Password")
input(type="submit" name="submit" value="Submit")
if links.select_device
a(hx-get=links.select_device hx-select="#innerTarget" hx-swap="outerHTML" href='#') Use a different device

View File

@ -0,0 +1,9 @@
extends base.pug
block content
main
form(hx-post=links.identifier_form hx-swap="outerHTML")
fieldset
input(type="hidden" name="state" value=state)
input(type="text" name="username" placeholder="Username")
input(type="submit" name="submit" value="Submit")

View File

@ -1,121 +0,0 @@
-- 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");

View File

@ -1,22 +0,0 @@
import { Module } from '@nestjs/common';
import { AuthorizationServerModule } from './authorization-server/authorization-server.module';
import { CacheModule } from './cache/cache.module';
import { ConfigModule } from './config/config.module';
import { GraphqlModule } from './graphql/graphql.module';
import { PrismaModule } from './prisma/prisma.module';
import { AuthDomainModule } from './auth-domain/auth-domain.module';
@Module({
imports: [
AuthorizationServerModule,
CacheModule,
ConfigModule,
GraphqlModule,
PrismaModule,
AuthDomainModule,
],
controllers: [],
providers: [],
})
export class AppModule {}

View File

@ -1,12 +0,0 @@
import { Module } from '@nestjs/common';
import { OauthController } from './oauth/oauth.controller';
import { IdentityModule } from '../identity-domain/identity.module';
@Module({
imports: [
IdentityModule,
],
controllers: [OauthController]
})
export class AuthorizationServerModule {}

View File

@ -1 +0,0 @@
export interface DeviceHandler {}

View File

@ -1,12 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import { DeviceHandler } from './device-handler.interface';
@Injectable()
export class PasswordHandler implements DeviceHandler {
constructor(
private readonly prismaService: PrismaService,
) {}
}

View File

@ -1,53 +0,0 @@
import { BadRequestException, Body, Controller, Post } from '@nestjs/common';
import * as Joi from 'joi';
import { LoginFirstContact, supportedDevices } from './login.interface';
import { Identity } from '../../identity-domain/identity.interface';
import { TokenManagementService } from '../token-management/token-management.service';
import { IdentityAuthDeviceService } from '../../identity-domain/identity-auth-device.service';
@Controller({
version: '1',
path: 'auth/:realm',
})
export class LoginController {
constructor(
private readonly identityAuthDeviceService: IdentityAuthDeviceService,
private readonly tokenManagementService: TokenManagementService,
) {}
@Post('login')
async login(
@Body() body: unknown,
): Promise<LoginFirstContact.Response> {
const { value: request, error } = Joi.object<LoginFirstContact.Request, true>({
state: Joi.string().required(),
username: Joi.string().required(),
}).validate(body, { allowUnknown: false, abortEarly: false });
if (error) {
throw new BadRequestException(error.message);
}
const authDevices = await this.identityAuthDeviceService.findByUsername(request.username);
return {
state: request.state,
availableDevices: authDevices.filter(dev => supportedDevices.includes(dev.deviceType as Identity.AuthDevice.Type)),
continuation: await this.tokenManagementService.generateContinuationToken(),
}
}
@Post('login/authenticate')
async loginAuthenticate(
) {
// parseLoginRequestBody
}
@Post('logout')
async logout() {
}
}

View File

@ -1,98 +0,0 @@
import { Auth, Identity } from '../../enumerations';
import * as Joi from 'joi';
import { declare } from '../../utils';
interface ILoginRequestMethod {
device: Identity.AuthDevice.Type;
}
export namespace LoginFirstContact {
export interface Request {
state: string;
username: string;
}
export interface Response {
state: string;
continuation: string;
availableDevices: Identity.AuthDevice.Type[];
}
}
export namespace LoginPassword {
export interface Request extends ILoginRequestMethod {
device: Identity.AuthDevice.Type.Password;
password: string;
state: string;
continuation?: string;
}
export type Response = Success | TwoFactorRequired | Failure | Timedout | DeviceLocked;
type Success = {
status: Auth.Login.ResponseStatus.Success;
state: string;
}
type TwoFactorRequired = {
status: Auth.Login.ResponseStatus.TwoFactorRequired;
state: string;
continuation: string;
availableDevices: Identity.AuthDevice.Type[];
}
type Failure = {
status: Auth.Login.ResponseStatus.Failure;
state: string;
continuation: string;
}
type Timedout = {
status: Auth.Login.ResponseStatus.Timedout;
state: string;
}
type DeviceLocked = {
status: Auth.Login.ResponseStatus.DeviceLocked;
state: string;
}
}
type LoginMethodRequests = LoginPassword.Request;
export const supportedDevices = [
Identity.AuthDevice.Type.Password,
]
const standardJoiValidationOptions: Joi.ValidationOptions = {
abortEarly: false,
allowUnknown: false,
}
const loginPasswordValidator = Joi.object<LoginPassword.Request, true>({
device: Joi.string().required().allow(Identity.AuthDevice.Type.Password),
password: Joi.string().required(),
state: Joi.string().required(),
continuation: Joi.string(),
}).options(standardJoiValidationOptions);
export const parseLoginRequestBody = (body: unknown): [null, LoginMethodRequests] | [Error, null] => {
const { error: error1, value: simpleRequest } = Joi.object<ILoginRequestMethod, true>({
device: Joi.string().required().allow(...supportedDevices),
}).validate(body);
if (error1) {
return [error1, null];
}
const { error: error2, value: request } = declare<Joi.ValidationResult, Identity.AuthDevice.Type>()
.when(simpleRequest.device)
.matches(Identity.AuthDevice.Type.Password).then(() => loginPasswordValidator.validate(body))
.resolveOrThrow();
if (error2) {
return [error2, null];
}
return [null, request];
}

View File

@ -1,6 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class LoginService {
}

View File

@ -1,24 +0,0 @@
import { Injectable } from '@nestjs/common';
import { CacheService } from '../../cache/cache.symbols';
import { ConfigService } from '../../config/config.service';
import { Identity, SystemSettings } from '../../enumerations';
@Injectable()
export class TokenManagementService {
private readonly signingSecret: string;
constructor(
private readonly configService: ConfigService,
private readonly cacheService: CacheService,
) {}
async generateContinuationToken(devicesUsed: Identity.AuthDevice.Type[]) {
}
private async signToken() {
const signingSecret = await this.configService.get(SystemSettings.Auth.TokenManagement.SigningSecret);
}
}

View File

@ -1,9 +0,0 @@
import { Identity } from '../../enumerations';
export namespace TokenManagement {
interface GenerateContinuationTokenRequest {
deviceUsed: Identity.AuthDevice.Type;
identityUserUrn: string;
previousContinuationToken?: string;
}
}

View File

@ -1,19 +0,0 @@
import { CacheModule as NestCacheModule } from '@nestjs/cache-manager';
import { Module } from '@nestjs/common';
import { CacheService } from './cache.symbols';
@Module({
imports: [
NestCacheModule.register(),
],
providers: [
{
provide: CacheService,
useFactory: (cache) => cache,
inject: [CacheService],
}
],
exports: [CacheService],
})
export class CacheModule {}

View File

@ -1,5 +0,0 @@
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
export type CacheService = Cache;
export const CacheService = CACHE_MANAGER;

View File

@ -1,10 +0,0 @@
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 {}

View File

@ -1,34 +0,0 @@
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];
}
}

View File

@ -1,21 +0,0 @@
import * as Joi from 'joi';
import { SystemSettings } from '../enumerations';
export interface Config {
[SystemSettings.Auth.Oauth2.Enabled]: boolean;
[SystemSettings.Auth.Oauth2.EncryptionSecret]: string;
[SystemSettings.Auth.TokenManagement.SigningSecret]: string;
[SystemSettings.Graphql.Debug]: boolean;
[SystemSettings.Graphql.IntrospectionEnabled]: boolean;
[SystemSettings.Graphql.PlaygroundEnabled]: boolean;
}
export const configValidator: Joi.ObjectSchema<Config> = Joi.object<Config, true>({
[SystemSettings.Auth.Oauth2.Enabled]: Joi.boolean().required(),
[SystemSettings.Auth.Oauth2.EncryptionSecret]: Joi.string().required(),
[SystemSettings.Auth.TokenManagement.SigningSecret]: Joi.string().required(),
[SystemSettings.Graphql.Debug]: Joi.boolean().required(),
[SystemSettings.Graphql.IntrospectionEnabled]: Joi.boolean().required(),
[SystemSettings.Graphql.PlaygroundEnabled]: Joi.boolean().required(),
});

View File

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

View File

@ -1,79 +0,0 @@
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,
})
});
}
}

View File

@ -1,13 +0,0 @@
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Module({
providers: [
PrismaService,
],
exports: [
],
})
export class DomainModule {}

View File

@ -1,29 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Identity } from './identity.interface';
@Injectable()
export class IdentityAuthDeviceService {
constructor(
private readonly prismaService: PrismaService,
) {}
async findByUsername(username: string): Promise<Identity.AuthDevice[]> {
const devices = await this.prismaService.identityAuthDevice.findMany({
include: {
hashMapPairs: true,
},
where: {
user: {
username,
}
}
});
return devices.map(d => Identity.AuthDevice.modelToEntity(d, d.hashMapPairs));
}
}

View File

@ -1,6 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class IdentityService {
}

View File

@ -1,63 +0,0 @@
import { IdentityAuthDevice, IdentityAuthDeviceNonNormalized } from '@prisma/client';
import { normalizePairs } from '../../utils';
export namespace Identity {
export interface AuthDevice {
urn: string;
userId: number;
deviceType: AuthDevice.Type;
createdAt: Date;
}
export namespace AuthDevice {
export enum Type {
Password = 'password',
ApplicationPassword = 'application_password',
}
export enum PasswordHashKey {
Expiry = 'expiry',
PasswordHashString = 'password_hash_string',
Locked = 'locked',
}
export interface Password extends AuthDevice {
expiry: Date;
passwordHashString: string;
locked: boolean;
}
export function modelToEntity(model: IdentityAuthDevice, hashMapPairs: IdentityAuthDeviceNonNormalized[]): AuthDevice {
const map = normalizePairs(hashMapPairs);
const entity: AuthDevice = {
urn: `urn:identity:auth-device:${model.id}`,
userId: model.userId,
deviceType: model.deviceType as Type,
createdAt: new Date(model.createdAt),
}
if (model.deviceType === AuthDevice.Type.Password) {
const passwordEntity: Password = {
...entity,
expiry: new Date(map[PasswordHashKey.Expiry] as string),
passwordHashString: map[PasswordHashKey.PasswordHashString] as string,
locked: map[PasswordHashKey.Locked] as boolean,
}
return passwordEntity;
}
return entity;
}
}
export namespace Group {
export enum Role {
SystemAdmin = 'SYSTEM_ADMIN',
RealmAdmin = 'REALM_ADMIN',
Standard = 'STANDARD',
}
}
}

View File

@ -1,15 +0,0 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
import { IdentityAuthDeviceService } from './identity-auth-device.service';
@Module({
imports: [PrismaModule],
providers: [
IdentityAuthDeviceService,
],
exports: [
IdentityAuthDeviceService,
],
})
export class IdentityModule {}

View File

@ -1,40 +0,0 @@
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import { _00ApplicationBootstrapDataMigration } from './data-migrations';
import { Identity } from './identity-domain/identity.interface';
import { CloudDav } from '../enumerations';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect();
await this.ensureEnumIntegrity();
}
async onModuleDestroy() {
await this.$disconnect();
}
async ensureEnumIntegrity() {
const prisma = this;
const enumsRegistered: [string, string[]][] = [
['enumIdentityGroupRole', Object.values<string>(Identity.Group.Role)],
['enumIdentityAuthDeviceType', Object.values<string>(Identity.AuthDevice.Type)],
['enumCloudDavResourceType', Object.values<string>(CloudDav.Resource.Type)],
];
for (const [dbRunner, known] of enumsRegistered) {
await prisma.$transaction(async (tx) => {
const result = await tx[dbRunner].findMany();
const existing = result.map(e => e.enumValue);
const missing = known.filter(k => !existing.includes(k));
await tx[dbRunner].createMany({
data: missing.map(enumValue => ({ enumValue })),
});
});
}
}
}

View File

@ -1,3 +0,0 @@
export * from './auth.enumerations';
export * from './cloud-dav.enumerations';
export * from './system-settings.enumerations';

View File

@ -1,23 +0,0 @@
export namespace SystemSettings {
export namespace Auth {
export enum Oauth2 {
Enabled = 'auth.oauth2.enabled',
EncryptionSecret = 'auth.oauth2.encryption_secret',
}
export enum TokenManagement {
SigningSecret = 'auth.token-management.signing_secret',
}
}
export enum Graphql {
Debug = 'graphql.debug.enabled',
IntrospectionEnabled = 'graphql.introspection.enabled',
PlaygroundEnabled = 'graphql.playground.enabled',
}
export namespace Dav {
export enum Contacts {
Enabled = 'dav.contacts.enabled',
}
}
}

View File

@ -1,90 +0,0 @@
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;
}
}

View File

@ -1,8 +0,0 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();

View File

@ -1,27 +0,0 @@
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();

View File

@ -1,58 +0,0 @@
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:',
}