WIP2
This commit is contained in:
parent
2410ef056c
commit
59540510b7
|
|
@ -11,7 +11,7 @@ type AuthOauth2Client {
|
||||||
clientSecret: String
|
clientSecret: String
|
||||||
|
|
||||||
Realm: AuthOauth2ClientToAuthRealmEdge
|
Realm: AuthOauth2ClientToAuthRealmEdge
|
||||||
Scopes: AuthOauth2ClientToAuthOauth2Scopes
|
Scopes: AuthOauth2ClientToAuthOauth2ScopesEdge
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthOauth2Scope {
|
type AuthOauth2Scope {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
# }
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
input UpdateSystemSettingInput {
|
input UpdateSystemSettingInput {
|
||||||
urn: ID!
|
urn: ID!
|
||||||
|
hashValueType: SystemSettingHashValueTypeEnum!
|
||||||
hashValue: String!
|
hashValue: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
node_modules
|
node_modules
|
||||||
.env
|
.env
|
||||||
__generated__
|
.generated
|
||||||
graphql.schema.json
|
graphql.schema.json
|
||||||
dist
|
dist
|
||||||
|
|
@ -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": {
|
||||||
|
|
@ -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",
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
@ -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");
|
||||||
|
|
@ -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])
|
|
||||||
}
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
@ -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 {}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {}
|
||||||
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export type LoadedConfig = Record<string, string>;
|
||||||
|
export const LoadedConfig = Symbol.for('LOADED_CONFIG');
|
||||||
|
|
@ -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(),
|
||||||
|
});
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {}
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||||
|
|
||||||
|
export class TooManyRequestsException extends HttpException {
|
||||||
|
constructor() {
|
||||||
|
super('Too Many Requests', HttpStatus.TOO_MANY_REQUESTS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { LoginModule } from './mvc/login/login.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [LoginModule],
|
||||||
|
})
|
||||||
|
export class HttpModule {}
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {}
|
||||||
|
|
@ -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,
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface GlobalProps {
|
||||||
|
user_settings: {
|
||||||
|
theme: 'light' | 'dark';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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 {}
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const safeJsonParse = (json: string): [Error, null] | [null, Object] => {
|
||||||
|
try {
|
||||||
|
return [null, JSON.parse(json)];
|
||||||
|
} catch (err) {
|
||||||
|
return [err, null];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(
|
||||||
|
|
@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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")
|
||||||
|
|
@ -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");
|
|
||||||
|
|
@ -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 {}
|
|
||||||
|
|
@ -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 {}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export interface DeviceHandler {}
|
|
||||||
|
|
@ -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,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
@ -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() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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];
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class LoginService {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import { Identity } from '../../enumerations';
|
|
||||||
|
|
||||||
export namespace TokenManagement {
|
|
||||||
interface GenerateContinuationTokenRequest {
|
|
||||||
deviceUsed: Identity.AuthDevice.Type;
|
|
||||||
identityUserUrn: string;
|
|
||||||
previousContinuationToken?: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 {}
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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 {}
|
|
||||||
|
|
@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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(),
|
|
||||||
});
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuthDomainService {}
|
|
||||||
|
|
@ -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,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { PrismaService } from './prisma.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [
|
|
||||||
PrismaService,
|
|
||||||
],
|
|
||||||
exports: [
|
|
||||||
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class DomainModule {}
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class IdentityService {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 {}
|
|
||||||
|
|
@ -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 })),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export * from './auth.enumerations';
|
|
||||||
export * from './cloud-dav.enumerations';
|
|
||||||
export * from './system-settings.enumerations';
|
|
||||||
|
|
@ -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',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
|
|
@ -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();
|
|
||||||
|
|
@ -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:',
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue