diff --git a/graphql/auth/auth.types.graphql b/graphql/auth/auth.types.graphql index 7682366..c7239c0 100644 --- a/graphql/auth/auth.types.graphql +++ b/graphql/auth/auth.types.graphql @@ -11,7 +11,7 @@ type AuthOauth2Client { clientSecret: String Realm: AuthOauth2ClientToAuthRealmEdge - Scopes: AuthOauth2ClientToAuthOauth2Scopes + Scopes: AuthOauth2ClientToAuthOauth2ScopesEdge } type AuthOauth2Scope { diff --git a/graphql/cloud-dav/cloud-dav.edges.graphql b/graphql/cloud-dav/cloud-dav.edges.graphqltodo similarity index 100% rename from graphql/cloud-dav/cloud-dav.edges.graphql rename to graphql/cloud-dav/cloud-dav.edges.graphqltodo diff --git a/graphql/cloud-dav/cloud-dav.enumerations.graphql b/graphql/cloud-dav/cloud-dav.enumerations.graphqltodo similarity index 100% rename from graphql/cloud-dav/cloud-dav.enumerations.graphql rename to graphql/cloud-dav/cloud-dav.enumerations.graphqltodo diff --git a/graphql/cloud-dav/cloud-dav.errors.graphql b/graphql/cloud-dav/cloud-dav.errors.graphqltodo similarity index 100% rename from graphql/cloud-dav/cloud-dav.errors.graphql rename to graphql/cloud-dav/cloud-dav.errors.graphqltodo diff --git a/graphql/identity/identity.queries.graphql b/graphql/identity/identity.queries.graphql index b510ade..73b624a 100644 --- a/graphql/identity/identity.queries.graphql +++ b/graphql/identity/identity.queries.graphql @@ -1,10 +1,10 @@ type IdentityUserOutput { - error: + error: String data: IdentityUser } -type Query { - identityUsers() - identityUser(urn: String!): IdentityUserOutput! - myUser -} \ No newline at end of file +# type Query { +# # identityUsers() +# # identityUser(urn: String!): IdentityUserOutput! +# # myUser +# } \ No newline at end of file diff --git a/graphql/system/system.mutations.graphql b/graphql/system/system.mutations.graphql index b4d6a8a..a1863b0 100644 --- a/graphql/system/system.mutations.graphql +++ b/graphql/system/system.mutations.graphql @@ -1,5 +1,6 @@ input UpdateSystemSettingInput { urn: ID! + hashValueType: SystemSettingHashValueTypeEnum! hashValue: String! } diff --git a/services/core/.eslintrc.js b/monolithic-backend/.eslintrc.js similarity index 100% rename from services/core/.eslintrc.js rename to monolithic-backend/.eslintrc.js diff --git a/services/core/.gitignore b/monolithic-backend/.gitignore similarity index 75% rename from services/core/.gitignore rename to monolithic-backend/.gitignore index df29594..ca5fb00 100644 --- a/services/core/.gitignore +++ b/monolithic-backend/.gitignore @@ -1,5 +1,5 @@ node_modules .env -__generated__ +.generated graphql.schema.json dist diff --git a/services/core/.prettierrc b/monolithic-backend/.prettierrc similarity index 100% rename from services/core/.prettierrc rename to monolithic-backend/.prettierrc diff --git a/services/core/CONTRIBUTE.md b/monolithic-backend/CONTRIBUTE.md similarity index 100% rename from services/core/CONTRIBUTE.md rename to monolithic-backend/CONTRIBUTE.md diff --git a/services/core/README.md b/monolithic-backend/README.md similarity index 100% rename from services/core/README.md rename to monolithic-backend/README.md diff --git a/services/core/codegen.ts b/monolithic-backend/codegen.ts similarity index 81% rename from services/core/codegen.ts rename to monolithic-backend/codegen.ts index 3259251..d7419ef 100644 --- a/services/core/codegen.ts +++ b/monolithic-backend/codegen.ts @@ -3,9 +3,9 @@ import type { CodegenConfig } from '@graphql-codegen/cli'; const config: CodegenConfig = { overwrite: true, - schema: "../../graphql", + schema: "../graphql", generates: { - "src/__generated__/graphql.ts": { + "src/.generated/graphql.ts": { plugins: ["typescript", "typescript-resolvers"] }, "./graphql.schema.json": { diff --git a/services/core/nest-cli.json b/monolithic-backend/nest-cli.json similarity index 100% rename from services/core/nest-cli.json rename to monolithic-backend/nest-cli.json diff --git a/services/core/package-lock.json b/monolithic-backend/package-lock.json similarity index 98% rename from services/core/package-lock.json rename to monolithic-backend/package-lock.json index bd85b3f..51658d7 100644 --- a/services/core/package-lock.json +++ b/monolithic-backend/package-lock.json @@ -11,16 +11,15 @@ "dependencies": { "@apollo/server": "^4.10.2", "@nestjs/apollo": "^12.1.0", - "@nestjs/cache-manager": "^2.2.2", "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/graphql": "^12.1.1", "@nestjs/platform-express": "^10.0.0", "@prisma/client": "^5.12.1", "bcrypt": "^5.1.1", - "cache-manager": "^5.5.1", "graphql": "^16.8.1", "joi": "17.6.4", + "pug": "^3.0.2", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, @@ -1001,7 +1000,6 @@ "version": "7.24.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -1010,7 +1008,6 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -1128,7 +1125,6 @@ "version": "7.24.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -1769,7 +1765,6 @@ "version": "7.24.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", - "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@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": { "version": "10.3.2", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz", @@ -5959,8 +5943,7 @@ "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" }, "node_modules/asn1js": { "version": "3.0.5", @@ -5976,6 +5959,11 @@ "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -6170,6 +6158,17 @@ "@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": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", @@ -6406,30 +6405,6 @@ "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": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -6569,6 +6544,14 @@ "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": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -6889,6 +6872,15 @@ "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": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -7253,6 +7245,11 @@ "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": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", @@ -8638,6 +8635,20 @@ "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -8954,7 +8965,6 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -8962,6 +8972,26 @@ "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": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -9033,6 +9063,26 @@ "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", @@ -9925,6 +9975,11 @@ "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10055,6 +10110,15 @@ "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": { "version": "4.5.4", "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", "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": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -10988,8 +11047,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-root": { "version": "0.1.1", @@ -11238,19 +11296,10 @@ "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dev": true, "dependencies": { "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": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -11276,6 +11325,118 @@ "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": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -11522,7 +11683,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -12405,7 +12565,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -12682,7 +12841,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, "engines": { "node": ">=4" } @@ -12706,6 +12864,11 @@ "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": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -13162,6 +13325,14 @@ "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": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -13356,6 +13527,20 @@ "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": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", diff --git a/services/core/package.json b/monolithic-backend/package.json similarity index 97% rename from services/core/package.json rename to monolithic-backend/package.json index 324d3c5..a734e16 100644 --- a/services/core/package.json +++ b/monolithic-backend/package.json @@ -25,16 +25,15 @@ "dependencies": { "@apollo/server": "^4.10.2", "@nestjs/apollo": "^12.1.0", - "@nestjs/cache-manager": "^2.2.2", "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/graphql": "^12.1.1", "@nestjs/platform-express": "^10.0.0", "@prisma/client": "^5.12.1", "bcrypt": "^5.1.1", - "cache-manager": "^5.5.1", "graphql": "^16.8.1", "joi": "17.6.4", + "pug": "^3.0.2", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" }, diff --git a/monolithic-backend/prisma/migrations/20240421050025_init/migration.sql b/monolithic-backend/prisma/migrations/20240421050025_init/migration.sql new file mode 100644 index 0000000..bb15060 --- /dev/null +++ b/monolithic-backend/prisma/migrations/20240421050025_init/migration.sql @@ -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"); diff --git a/services/core/prisma/migrations/migration_lock.toml b/monolithic-backend/prisma/migrations/migration_lock.toml similarity index 100% rename from services/core/prisma/migrations/migration_lock.toml rename to monolithic-backend/prisma/migrations/migration_lock.toml diff --git a/services/core/prisma/schema.prisma b/monolithic-backend/prisma/schema.prisma similarity index 85% rename from services/core/prisma/schema.prisma rename to monolithic-backend/prisma/schema.prisma index 7c4edcf..129a26d 100644 --- a/services/core/prisma/schema.prisma +++ b/monolithic-backend/prisma/schema.prisma @@ -4,15 +4,16 @@ generator client { datasource db { provider = "sqlite" - url = "file:../../../data/core.db" + url = "file:../../data/core.db" } // // Namespace: System // model SystemSetting { - hashKey String @id - hashValue String + hashKey String @id + hashValueType String + hashValue String } model SystemPostMigration { @@ -30,6 +31,7 @@ model AuthRealm { oauth2Clients AuthOauth2Client[] groups IdentityGroup[] + users IdentityUser[] profileAttributeNames IdentityProfileAttributeName[] roles AuthRole[] } @@ -43,6 +45,7 @@ model AuthOauth2Client { clientId String clientSecret String? + consentRequired Boolean @default(false) authorizationCodeFlowEnabled Boolean @default(false) resourceOwnerPasswordCredentialsFlowEnabled Boolean @default(false) clientCredentialsFlowEnabled Boolean @default(false) @@ -97,6 +100,16 @@ model AuthRole { @@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 // @@ -138,6 +151,9 @@ model IdentityUser { externalId String @unique @default(uuid()) username String @unique + realmId Int + realm AuthRealm @relation(fields: [realmId], references: [id]) + groups IdentityGroupToIdentityUser[] profileHashMapPairs IdentityProfileNonNormalized[] emails IdentityUserEmails[] @@ -163,8 +179,8 @@ model IdentityProfileNonNormalized { attributeNameId Int attributeName IdentityProfileAttributeName @relation(fields: [attributeNameId], references: [id]) - hashValue String - createdAt DateTime @default(now()) + attributeValue String + createdAt DateTime @default(now()) @@id([userId, attributeNameId]) } @@ -186,7 +202,7 @@ model EnumIdentityAuthDeviceType { } model IdentityAuthDevice { - id Int @id @default(autoincrement()) + id String @id @default(uuid()) userId Int user IdentityUser @relation(fields: [userId], references: [id]) @@ -194,25 +210,14 @@ model IdentityAuthDevice { deviceType String deviceTypeRelation EnumIdentityAuthDeviceType @relation(fields: [deviceType], references: [enumValue]) - createdAt DateTime @default(now()) - - hashMapPairs IdentityAuthDeviceNonNormalized[] + attributes String + preferred Boolean + createdAt DateTime @default(now()) @@index([userId]) @@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 // @@ -231,18 +236,7 @@ model CloudDavResource { resourceType String resourceTypeRelation EnumCloudDavResourceType @relation(fields: [resourceType], references: [enumValue]) - hashMapPairs CloudDavResourceNonNormalized[] + attributes String @@index([identityGroupId]) } - -model CloudDavResourceNonNormalized { - davResourceId String - davResource CloudDavResource @relation(fields: [davResourceId], references: [id]) - - hashKey String - hashValue String - createdAt DateTime @default(now()) - - @@id([davResourceId, hashKey]) -} diff --git a/monolithic-backend/src/app.module.ts b/monolithic-backend/src/app.module.ts new file mode 100644 index 0000000..8bdfa32 --- /dev/null +++ b/monolithic-backend/src/app.module.ts @@ -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 {} diff --git a/monolithic-backend/src/cache/cache.module.ts b/monolithic-backend/src/cache/cache.module.ts new file mode 100644 index 0000000..323f6a3 --- /dev/null +++ b/monolithic-backend/src/cache/cache.module.ts @@ -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 {} diff --git a/monolithic-backend/src/cache/cache.service.ts b/monolithic-backend/src/cache/cache.service.ts new file mode 100644 index 0000000..3aba64f --- /dev/null +++ b/monolithic-backend/src/cache/cache.service.ts @@ -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 { + return this.cacheDriver.get(key); + } + + async set(key: string, val: string, ttl?: number): Promise { + await this.cacheDriver.set(key, val); + + if (ttl) { + await this.cacheDriver.expire(key, ttl); + } + } + + async exists(key: string): Promise { + return await this.cacheDriver.exists(key) > 0; + } + + async del(key: string): Promise { + await this.cacheDriver.del(key); + } +} diff --git a/monolithic-backend/src/cache/cache.symbols.ts b/monolithic-backend/src/cache/cache.symbols.ts new file mode 100644 index 0000000..e3dafd3 --- /dev/null +++ b/monolithic-backend/src/cache/cache.symbols.ts @@ -0,0 +1,8 @@ +export interface CacheDriver { + get(key: string): Promise; + set(key: string, val: string): Promise<'OK'>; + exists(...keys: string[]): Promise; + expire(key: string, seconds: number): Promise; + del(key: string): Promise; +} +export const CacheDriver = Symbol.for('CACHE_DRIVER'); diff --git a/monolithic-backend/src/cache/in-memory.driver.ts b/monolithic-backend/src/cache/in-memory.driver.ts new file mode 100644 index 0000000..d93c858 --- /dev/null +++ b/monolithic-backend/src/cache/in-memory.driver.ts @@ -0,0 +1,42 @@ +import { DateUtil } from '../utils'; +import { CacheDriver } from './cache.symbols'; + +export class InMemoryDriver implements CacheDriver { + + private readonly cache: Record = {}; + + get(key: string): Promise { + + 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 { + return Promise.resolve( + keys.reduce((acc, k) => { + if (k in this.cache) { + return acc + 1; + } + return acc; + }, 0) + ); + } + + expire(key: string, seconds: number): Promise { + const dateUtil = DateUtil.fromDate(new Date()).addNSeconds(seconds); + this.cache[key].ttl = dateUtil; + return Promise.resolve(1); + } + + del(key: string): Promise { + delete this.cache[key]; + return Promise.resolve(1); + } +} diff --git a/monolithic-backend/src/config/config.module.ts b/monolithic-backend/src/config/config.module.ts new file mode 100644 index 0000000..78a914b --- /dev/null +++ b/monolithic-backend/src/config/config.module.ts @@ -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 {} diff --git a/monolithic-backend/src/config/config.service.ts b/monolithic-backend/src/config/config.service.ts new file mode 100644 index 0000000..6b39d65 --- /dev/null +++ b/monolithic-backend/src/config/config.service.ts @@ -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(key: K): SystemSettings.Config[K] { + return this._config[key]; + } +} diff --git a/monolithic-backend/src/config/config.symbol.ts b/monolithic-backend/src/config/config.symbol.ts new file mode 100644 index 0000000..e503e12 --- /dev/null +++ b/monolithic-backend/src/config/config.symbol.ts @@ -0,0 +1,2 @@ +export type LoadedConfig = Record; +export const LoadedConfig = Symbol.for('LOADED_CONFIG'); diff --git a/monolithic-backend/src/config/config.validator.ts b/monolithic-backend/src/config/config.validator.ts new file mode 100644 index 0000000..afa9bb5 --- /dev/null +++ b/monolithic-backend/src/config/config.validator.ts @@ -0,0 +1,15 @@ +import * as Joi from 'joi'; + +import { SystemSettings } from '../domain/system-settings.types'; + +export const configValidator: Joi.ObjectSchema = Joi.object({ + [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(), +}); diff --git a/services/core/src/enumerations/auth.enumerations.ts b/monolithic-backend/src/domain/authentication.types.ts similarity index 70% rename from services/core/src/enumerations/auth.enumerations.ts rename to monolithic-backend/src/domain/authentication.types.ts index 5344a8b..b4f3cdc 100644 --- a/services/core/src/enumerations/auth.enumerations.ts +++ b/monolithic-backend/src/domain/authentication.types.ts @@ -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 enum ResponseStatus { @@ -8,6 +18,20 @@ export namespace Auth { Timedout = 'timedout', 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 { diff --git a/services/core/src/enumerations/cloud-dav.enumerations.ts b/monolithic-backend/src/domain/cloud-dav.types.ts similarity index 100% rename from services/core/src/enumerations/cloud-dav.enumerations.ts rename to monolithic-backend/src/domain/cloud-dav.types.ts diff --git a/monolithic-backend/src/domain/identity.types.ts b/monolithic-backend/src/domain/identity.types.ts new file mode 100644 index 0000000..d6f6459 --- /dev/null +++ b/monolithic-backend/src/domain/identity.types.ts @@ -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', + } + } +} diff --git a/monolithic-backend/src/domain/serializable.type.ts b/monolithic-backend/src/domain/serializable.type.ts new file mode 100644 index 0000000..ab13fb5 --- /dev/null +++ b/monolithic-backend/src/domain/serializable.type.ts @@ -0,0 +1,18 @@ +export abstract class Serializable { + + toJson(): string { + return JSON.stringify(this); + } + + static fromJson(): ThisType { + 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]; + } + } +} diff --git a/monolithic-backend/src/domain/system-settings.types.ts b/monolithic-backend/src/domain/system-settings.types.ts new file mode 100644 index 0000000..ad2b08c --- /dev/null +++ b/monolithic-backend/src/domain/system-settings.types.ts @@ -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', + } + } +} diff --git a/services/core/src/graphql/graphql.module.ts b/monolithic-backend/src/graphql-server/graphql-server.module.ts similarity index 78% rename from services/core/src/graphql/graphql.module.ts rename to monolithic-backend/src/graphql-server/graphql-server.module.ts index ed3e523..6513ee0 100644 --- a/services/core/src/graphql/graphql.module.ts +++ b/monolithic-backend/src/graphql-server/graphql-server.module.ts @@ -3,22 +3,22 @@ import { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import { ConfigService } from '../config/config.service'; -import { SystemSettings } from '../enumerations'; import { SystemSettingsResolver } from './system-settings.resolver'; -import { PrismaModule } from '../prisma/prisma.module'; import { ConfigModule } from '../config/config.module'; +import { SystemSettings } from '../domain/system-settings.types'; +import { PersistenceModule } from '../persistence/persistence.module'; @Module({ imports: [ ConfigModule, - PrismaModule, + PersistenceModule, GraphQLModule.forRootAsync({ driver: ApolloDriver, useFactory: async (configService: ConfigService) => ({ debug: await configService.get(SystemSettings.Graphql.Debug), playground: await configService.get(SystemSettings.Graphql.PlaygroundEnabled), introspection: await configService.get(SystemSettings.Graphql.IntrospectionEnabled), - typePaths: ['../../graphql/**/*.graphql'], + typePaths: ['../graphql/**/*.graphql'], }), inject: [ConfigService], imports: [ConfigModule], @@ -28,4 +28,4 @@ import { ConfigModule } from '../config/config.module'; SystemSettingsResolver, ] }) -export class GraphqlModule {} +export class GraphqlServerModule {} diff --git a/monolithic-backend/src/graphql-server/system-settings.resolver.ts b/monolithic-backend/src/graphql-server/system-settings.resolver.ts new file mode 100644 index 0000000..c55ae13 --- /dev/null +++ b/monolithic-backend/src/graphql-server/system-settings.resolver.ts @@ -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]: '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 { + 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 { + 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, + } + } + } +} diff --git a/monolithic-backend/src/http/exceptions/too-many-requests.exception.ts b/monolithic-backend/src/http/exceptions/too-many-requests.exception.ts new file mode 100644 index 0000000..3374738 --- /dev/null +++ b/monolithic-backend/src/http/exceptions/too-many-requests.exception.ts @@ -0,0 +1,7 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; + +export class TooManyRequestsException extends HttpException { + constructor() { + super('Too Many Requests', HttpStatus.TOO_MANY_REQUESTS); + } +} diff --git a/monolithic-backend/src/http/http.module.ts b/monolithic-backend/src/http/http.module.ts new file mode 100644 index 0000000..45dfa0a --- /dev/null +++ b/monolithic-backend/src/http/http.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; + +import { LoginModule } from './mvc/login/login.module'; + +@Module({ + imports: [LoginModule], +}) +export class HttpModule {} diff --git a/monolithic-backend/src/http/mvc/login/access-attempt.service.ts b/monolithic-backend/src/http/mvc/login/access-attempt.service.ts new file mode 100644 index 0000000..552860a --- /dev/null +++ b/monolithic-backend/src/http/mvc/login/access-attempt.service.ts @@ -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 { + + 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 { + + 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, + } + } +} diff --git a/monolithic-backend/src/http/mvc/login/context.decorator.ts b/monolithic-backend/src/http/mvc/login/context.decorator.ts new file mode 100644 index 0000000..f72011b --- /dev/null +++ b/monolithic-backend/src/http/mvc/login/context.decorator.ts @@ -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({ + 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, + }; + }, +); diff --git a/monolithic-backend/src/http/mvc/login/login-context.interceptor.ts b/monolithic-backend/src/http/mvc/login/login-context.interceptor.ts new file mode 100644 index 0000000..9cde79f --- /dev/null +++ b/monolithic-backend/src/http/mvc/login/login-context.interceptor.ts @@ -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): Promise> { + 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(); + } +} diff --git a/monolithic-backend/src/http/mvc/login/login.controller.ts b/monolithic-backend/src/http/mvc/login/login.controller.ts new file mode 100644 index 0000000..3c33d22 --- /dev/null +++ b/monolithic-backend/src/http/mvc/login/login.controller.ts @@ -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 { + + 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() { + + } +} diff --git a/monolithic-backend/src/http/mvc/login/login.module.ts b/monolithic-backend/src/http/mvc/login/login.module.ts new file mode 100644 index 0000000..091c041 --- /dev/null +++ b/monolithic-backend/src/http/mvc/login/login.module.ts @@ -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 {} diff --git a/monolithic-backend/src/http/mvc/login/login.types.ts b/monolithic-backend/src/http/mvc/login/login.types.ts new file mode 100644 index 0000000..e7bc2c2 --- /dev/null +++ b/monolithic-backend/src/http/mvc/login/login.types.ts @@ -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.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, +] diff --git a/monolithic-backend/src/http/mvc/mvc.types.ts b/monolithic-backend/src/http/mvc/mvc.types.ts new file mode 100644 index 0000000..8b7a96a --- /dev/null +++ b/monolithic-backend/src/http/mvc/mvc.types.ts @@ -0,0 +1,5 @@ +export interface GlobalProps { + user_settings: { + theme: 'light' | 'dark'; + } +} diff --git a/services/core/src/authorization-server/oauth/oauth.controller.ts b/monolithic-backend/src/http/mvc/oauth/oauth.controller.ts similarity index 100% rename from services/core/src/authorization-server/oauth/oauth.controller.ts rename to monolithic-backend/src/http/mvc/oauth/oauth.controller.ts diff --git a/services/core/src/authorization-server/oauth/oauth.interface.ts b/monolithic-backend/src/http/mvc/oauth/oauth.interface.ts similarity index 77% rename from services/core/src/authorization-server/oauth/oauth.interface.ts rename to monolithic-backend/src/http/mvc/oauth/oauth.interface.ts index 8e2f31d..6b06c16 100644 --- a/services/core/src/authorization-server/oauth/oauth.interface.ts +++ b/monolithic-backend/src/http/mvc/oauth/oauth.interface.ts @@ -1,15 +1,15 @@ -import { Auth } from '../../enumerations/auth.enumerations'; +import { Authentication } from '../../../domain/authentication.types'; interface ErrorResponse { state?: string; - error: Auth.Oauth2.Error; + error: Authentication.Oauth2.Error; error_description?: string; error_uri?: string; } namespace AuthorizationCode { interface AuthorizationRequest { - response_type: Auth.Oauth2.ResponseType.Code; + response_type: Authentication.Oauth2.ResponseType.Code; client_id: string; redirect_uri?: string; scope?: string; @@ -22,7 +22,7 @@ namespace AuthorizationCode { } interface AccessTokenRequest { - grant_type: Auth.Oauth2.AuthorizationGrant.AuthorizationCode; + grant_type: Authentication.Oauth2.AuthorizationGrant.AuthorizationCode; code: string; redirect_uri?: string; client_id: string; @@ -41,7 +41,7 @@ namespace AuthorizationCode { // Basic base64(clientId:clientSecret) namespace ResourceOwner { interface AccessTokenRequest { - grant_type: Auth.Oauth2.GrantType.Password; + grant_type: Authentication.Oauth2.GrantType.Password; username: string; password: string; scope?: string; @@ -58,7 +58,7 @@ namespace ResourceOwner { // `confidential` only namespace ClientCredentials { interface AccessTokenRequest { - // grant_type: Auth.Oauth2.GrantType.ClientCredentials; + // grant_type: Authentication.Oauth2.GrantType.ClientCredentials; scope?: string; } diff --git a/monolithic-backend/src/main.ts b/monolithic-backend/src/main.ts new file mode 100644 index 0000000..49cf485 --- /dev/null +++ b/monolithic-backend/src/main.ts @@ -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(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(); diff --git a/monolithic-backend/src/persistence/auth-access-attempt.dao.ts b/monolithic-backend/src/persistence/auth-access-attempt.dao.ts new file mode 100644 index 0000000..64f2293 --- /dev/null +++ b/monolithic-backend/src/persistence/auth-access-attempt.dao.ts @@ -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 { + await this.prismaService.authAccessAttempt.create({ + data, + }); + } +} diff --git a/monolithic-backend/src/persistence/data-migrations/00-application-bootstrap-data-migration.ts b/monolithic-backend/src/persistence/data-migrations/00-application-bootstrap-data-migration.ts new file mode 100644 index 0000000..2430e5c --- /dev/null +++ b/monolithic-backend/src/persistence/data-migrations/00-application-bootstrap-data-migration.ts @@ -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, + }); + }); + } + +} diff --git a/services/core/src/domain/data-migrations/data-migration.interface.ts b/monolithic-backend/src/persistence/data-migrations/data-migration.interface.ts similarity index 57% rename from services/core/src/domain/data-migrations/data-migration.interface.ts rename to monolithic-backend/src/persistence/data-migrations/data-migration.interface.ts index 762f3a1..4cd7160 100644 --- a/services/core/src/domain/data-migrations/data-migration.interface.ts +++ b/monolithic-backend/src/persistence/data-migrations/data-migration.interface.ts @@ -1,4 +1,4 @@ -import { PrismaService } from '../prisma.service'; +import { PrismaService } from '../../persistence/prisma.service'; export interface DataMigration { name: string; diff --git a/services/core/src/domain/data-migrations/index.ts b/monolithic-backend/src/persistence/data-migrations/index.ts similarity index 100% rename from services/core/src/domain/data-migrations/index.ts rename to monolithic-backend/src/persistence/data-migrations/index.ts diff --git a/monolithic-backend/src/persistence/identity-auth-device.dao.ts b/monolithic-backend/src/persistence/identity-auth-device.dao.ts new file mode 100644 index 0000000..ccce5ec --- /dev/null +++ b/monolithic-backend/src/persistence/identity-auth-device.dao.ts @@ -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 { + + 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 { + 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; + } +} diff --git a/monolithic-backend/src/persistence/persistence.module.ts b/monolithic-backend/src/persistence/persistence.module.ts new file mode 100644 index 0000000..1612746 --- /dev/null +++ b/monolithic-backend/src/persistence/persistence.module.ts @@ -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 {} diff --git a/monolithic-backend/src/persistence/prisma.service.ts b/monolithic-backend/src/persistence/prisma.service.ts new file mode 100644 index 0000000..623d5e0 --- /dev/null +++ b/monolithic-backend/src/persistence/prisma.service.ts @@ -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(); + } +} diff --git a/monolithic-backend/src/persistence/system-settings.dao.ts b/monolithic-backend/src/persistence/system-settings.dao.ts new file mode 100644 index 0000000..2276c52 --- /dev/null +++ b/monolithic-backend/src/persistence/system-settings.dao.ts @@ -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; + valueMap: Record; +} + +@Injectable() +export class SystemSettingsDao { + + constructor( + private readonly prismaService: PrismaService, + ) {} + + async getSettings(): Promise { + 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 { + return await this.prismaService.systemSetting.count({ + where: { + hashKey + } + }) > 0; + } + + async setSetting(hashKey: keyof SystemSettings.Config, hashValue: string | boolean | number): Promise { + 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 { + await this.prismaService.systemSetting.update({ + where: { hashKey }, + data: { + hashValue: hashValue, + hashValueType: hashValueType, + }, + }); + } +} diff --git a/monolithic-backend/src/prisma-post-migrations.ts b/monolithic-backend/src/prisma-post-migrations.ts new file mode 100644 index 0000000..2cf7027 --- /dev/null +++ b/monolithic-backend/src/prisma-post-migrations.ts @@ -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(Identity.Group.Role)], + ['enumIdentityAuthDeviceType', Object.values(Identity.AuthDevice.Type)], + ['enumCloudDavResourceType', Object.values(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(); diff --git a/monolithic-backend/src/token-management/token-management.module.ts b/monolithic-backend/src/token-management/token-management.module.ts new file mode 100644 index 0000000..247a7cf --- /dev/null +++ b/monolithic-backend/src/token-management/token-management.module.ts @@ -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 {} diff --git a/monolithic-backend/src/token-management/token-management.service.ts b/monolithic-backend/src/token-management/token-management.service.ts new file mode 100644 index 0000000..e995429 --- /dev/null +++ b/monolithic-backend/src/token-management/token-management.service.ts @@ -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({ + 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, + } + } +} diff --git a/monolithic-backend/src/utils/date-util.ts b/monolithic-backend/src/utils/date-util.ts new file mode 100644 index 0000000..5cc812a --- /dev/null +++ b/monolithic-backend/src/utils/date-util.ts @@ -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; + } +} diff --git a/services/core/src/utils/index.ts b/monolithic-backend/src/utils/index.ts similarity index 60% rename from services/core/src/utils/index.ts rename to monolithic-backend/src/utils/index.ts index b1e0c2b..ef847de 100644 --- a/services/core/src/utils/index.ts +++ b/monolithic-backend/src/utils/index.ts @@ -1,4 +1,5 @@ -export * from './normalize-hash-set'; +export * from './date-util'; +export * from './safe-json-parse'; export * from './secure-string'; export * from './snake-to-camel'; export * from './when-clause'; diff --git a/monolithic-backend/src/utils/safe-json-parse.ts b/monolithic-backend/src/utils/safe-json-parse.ts new file mode 100644 index 0000000..4a98d5a --- /dev/null +++ b/monolithic-backend/src/utils/safe-json-parse.ts @@ -0,0 +1,7 @@ +export const safeJsonParse = (json: string): [Error, null] | [null, Object] => { + try { + return [null, JSON.parse(json)]; + } catch (err) { + return [err, null]; + } +} diff --git a/services/core/src/utils/secure-string.ts b/monolithic-backend/src/utils/secure-string.ts similarity index 85% rename from services/core/src/utils/secure-string.ts rename to monolithic-backend/src/utils/secure-string.ts index 02fe08f..e55f592 100644 --- a/services/core/src/utils/secure-string.ts +++ b/monolithic-backend/src/utils/secure-string.ts @@ -1,4 +1,5 @@ import * as bcrypt from 'bcrypt'; +import * as crypto from 'node:crypto'; // https://passlib.readthedocs.io/en/stable/modular_crypt_format.html#mcf-identifiers type OSDefinedHashPrefix = | @@ -57,6 +58,18 @@ const algImplementationMap: Record = { } } +export enum JWA { + HSA256 = 'HSA256', +} + +type SigningFunction = (rawString: string, signingComponent: string) => string; + +const jwaImplementationMap: Record = { + [JWA.HSA256]: function (rawString: string, signingComponent: string) { + return crypto.createHmac('sha-256', signingComponent).update(rawString).digest('base64url'); + }, +} + export class SecureStringUtil { 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 { constructor( diff --git a/services/core/src/utils/snake-to-camel.ts b/monolithic-backend/src/utils/snake-to-camel.ts similarity index 100% rename from services/core/src/utils/snake-to-camel.ts rename to monolithic-backend/src/utils/snake-to-camel.ts diff --git a/services/core/src/utils/when-clause.ts b/monolithic-backend/src/utils/when-clause.ts similarity index 100% rename from services/core/src/utils/when-clause.ts rename to monolithic-backend/src/utils/when-clause.ts diff --git a/services/core/tsconfig.build.json b/monolithic-backend/tsconfig.build.json similarity index 100% rename from services/core/tsconfig.build.json rename to monolithic-backend/tsconfig.build.json diff --git a/services/core/tsconfig.json b/monolithic-backend/tsconfig.json similarity index 81% rename from services/core/tsconfig.json rename to monolithic-backend/tsconfig.json index 95f5641..d9661dc 100644 --- a/services/core/tsconfig.json +++ b/monolithic-backend/tsconfig.json @@ -9,13 +9,16 @@ "target": "ES2021", "sourceMap": true, "outDir": "./dist", - "baseUrl": "./", + "baseUrl": "./src", "incremental": true, "skipLibCheck": true, "strictNullChecks": false, "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false - } + "noFallthroughCasesInSwitch": false, + }, + "exclude": [ + "codegen.ts" + ] } diff --git a/monolithic-backend/views/base.pug b/monolithic-backend/views/base.pug new file mode 100644 index 0000000..3c1f9ba --- /dev/null +++ b/monolithic-backend/views/base.pug @@ -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 diff --git a/monolithic-backend/views/login-password-challenge.pug b/monolithic-backend/views/login-password-challenge.pug new file mode 100644 index 0000000..a61e0e5 --- /dev/null +++ b/monolithic-backend/views/login-password-challenge.pug @@ -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 diff --git a/monolithic-backend/views/login-view.pug b/monolithic-backend/views/login-view.pug new file mode 100644 index 0000000..a3f907b --- /dev/null +++ b/monolithic-backend/views/login-view.pug @@ -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") diff --git a/services/core/prisma/migrations/20240414000453_init/migration.sql b/services/core/prisma/migrations/20240414000453_init/migration.sql deleted file mode 100644 index bb85ebd..0000000 --- a/services/core/prisma/migrations/20240414000453_init/migration.sql +++ /dev/null @@ -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"); diff --git a/services/core/src/app.module.ts b/services/core/src/app.module.ts deleted file mode 100644 index b7d880a..0000000 --- a/services/core/src/app.module.ts +++ /dev/null @@ -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 {} diff --git a/services/core/src/authorization-server/authorization-server.module.ts b/services/core/src/authorization-server/authorization-server.module.ts deleted file mode 100644 index 6180e25..0000000 --- a/services/core/src/authorization-server/authorization-server.module.ts +++ /dev/null @@ -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 {} diff --git a/services/core/src/authorization-server/login/device-handlers/device-handler.interface.ts b/services/core/src/authorization-server/login/device-handlers/device-handler.interface.ts deleted file mode 100644 index ca19a61..0000000 --- a/services/core/src/authorization-server/login/device-handlers/device-handler.interface.ts +++ /dev/null @@ -1 +0,0 @@ -export interface DeviceHandler {} diff --git a/services/core/src/authorization-server/login/device-handlers/password.handler.ts b/services/core/src/authorization-server/login/device-handlers/password.handler.ts deleted file mode 100644 index 287290d..0000000 --- a/services/core/src/authorization-server/login/device-handlers/password.handler.ts +++ /dev/null @@ -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, - ) {} -} diff --git a/services/core/src/authorization-server/login/login.controller.ts b/services/core/src/authorization-server/login/login.controller.ts deleted file mode 100644 index f9afddd..0000000 --- a/services/core/src/authorization-server/login/login.controller.ts +++ /dev/null @@ -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 { - - const { value: request, error } = Joi.object({ - 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() { - - } -} diff --git a/services/core/src/authorization-server/login/login.interface.ts b/services/core/src/authorization-server/login/login.interface.ts deleted file mode 100644 index b3236cc..0000000 --- a/services/core/src/authorization-server/login/login.interface.ts +++ /dev/null @@ -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({ - 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({ - device: Joi.string().required().allow(...supportedDevices), - }).validate(body); - - if (error1) { - return [error1, null]; - } - - const { error: error2, value: request } = declare() - .when(simpleRequest.device) - .matches(Identity.AuthDevice.Type.Password).then(() => loginPasswordValidator.validate(body)) - .resolveOrThrow(); - - if (error2) { - return [error2, null]; - } - - return [null, request]; -} diff --git a/services/core/src/authorization-server/login/login.service.ts b/services/core/src/authorization-server/login/login.service.ts deleted file mode 100644 index 6ad0802..0000000 --- a/services/core/src/authorization-server/login/login.service.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class LoginService { - -} diff --git a/services/core/src/authorization-server/token-management/token-management.service.ts b/services/core/src/authorization-server/token-management/token-management.service.ts deleted file mode 100644 index 54828c5..0000000 --- a/services/core/src/authorization-server/token-management/token-management.service.ts +++ /dev/null @@ -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); - } -} diff --git a/services/core/src/authorization-server/token-management/token-mangement.interface.ts b/services/core/src/authorization-server/token-management/token-mangement.interface.ts deleted file mode 100644 index f6c7674..0000000 --- a/services/core/src/authorization-server/token-management/token-mangement.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Identity } from '../../enumerations'; - -export namespace TokenManagement { - interface GenerateContinuationTokenRequest { - deviceUsed: Identity.AuthDevice.Type; - identityUserUrn: string; - previousContinuationToken?: string; - } -} diff --git a/services/core/src/cache/cache.module.ts b/services/core/src/cache/cache.module.ts deleted file mode 100644 index 35aa3ce..0000000 --- a/services/core/src/cache/cache.module.ts +++ /dev/null @@ -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 {} diff --git a/services/core/src/cache/cache.symbols.ts b/services/core/src/cache/cache.symbols.ts deleted file mode 100644 index b177b35..0000000 --- a/services/core/src/cache/cache.symbols.ts +++ /dev/null @@ -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; diff --git a/services/core/src/config/config.module.ts b/services/core/src/config/config.module.ts deleted file mode 100644 index a3a7e93..0000000 --- a/services/core/src/config/config.module.ts +++ /dev/null @@ -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 {} diff --git a/services/core/src/config/config.service.ts b/services/core/src/config/config.service.ts deleted file mode 100644 index 333ad33..0000000 --- a/services/core/src/config/config.service.ts +++ /dev/null @@ -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;; - - 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(key: K): Promise { - const config = await this._config; - return config[key]; - } -} diff --git a/services/core/src/config/config.struct.ts b/services/core/src/config/config.struct.ts deleted file mode 100644 index d725517..0000000 --- a/services/core/src/config/config.struct.ts +++ /dev/null @@ -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 = Joi.object({ - [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(), -}); diff --git a/services/core/src/domain/auth-domain/auth-domain.service.ts b/services/core/src/domain/auth-domain/auth-domain.service.ts deleted file mode 100644 index d7eb5f0..0000000 --- a/services/core/src/domain/auth-domain/auth-domain.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AuthDomainService {} diff --git a/services/core/src/domain/data-migrations/00-application-bootstrap-data-migration.ts b/services/core/src/domain/data-migrations/00-application-bootstrap-data-migration.ts deleted file mode 100644 index 1735839..0000000 --- a/services/core/src/domain/data-migrations/00-application-bootstrap-data-migration.ts +++ /dev/null @@ -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, - }) - }); - } - -} diff --git a/services/core/src/domain/domain.module.ts b/services/core/src/domain/domain.module.ts deleted file mode 100644 index 0f0c06a..0000000 --- a/services/core/src/domain/domain.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { PrismaService } from './prisma.service'; - -@Module({ - providers: [ - PrismaService, - ], - exports: [ - - ], -}) -export class DomainModule {} diff --git a/services/core/src/domain/identity-domain/identity-auth-device.service.ts b/services/core/src/domain/identity-domain/identity-auth-device.service.ts deleted file mode 100644 index 232f6ad..0000000 --- a/services/core/src/domain/identity-domain/identity-auth-device.service.ts +++ /dev/null @@ -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 { - - const devices = await this.prismaService.identityAuthDevice.findMany({ - include: { - hashMapPairs: true, - }, - where: { - user: { - username, - } - } - }); - - return devices.map(d => Identity.AuthDevice.modelToEntity(d, d.hashMapPairs)); - } -} diff --git a/services/core/src/domain/identity-domain/identity-user.service.ts b/services/core/src/domain/identity-domain/identity-user.service.ts deleted file mode 100644 index db24835..0000000 --- a/services/core/src/domain/identity-domain/identity-user.service.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class IdentityService { - -} diff --git a/services/core/src/domain/identity-domain/identity.interface.ts b/services/core/src/domain/identity-domain/identity.interface.ts deleted file mode 100644 index 2ea4ecf..0000000 --- a/services/core/src/domain/identity-domain/identity.interface.ts +++ /dev/null @@ -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', - } - } -} diff --git a/services/core/src/domain/identity-domain/identity.module.ts b/services/core/src/domain/identity-domain/identity.module.ts deleted file mode 100644 index d5b0052..0000000 --- a/services/core/src/domain/identity-domain/identity.module.ts +++ /dev/null @@ -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 {} diff --git a/services/core/src/domain/prisma.service.ts b/services/core/src/domain/prisma.service.ts deleted file mode 100644 index 67e65e1..0000000 --- a/services/core/src/domain/prisma.service.ts +++ /dev/null @@ -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(Identity.Group.Role)], - ['enumIdentityAuthDeviceType', Object.values(Identity.AuthDevice.Type)], - ['enumCloudDavResourceType', Object.values(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 })), - }); - }); - } - } -} diff --git a/services/core/src/enumerations/index.ts b/services/core/src/enumerations/index.ts deleted file mode 100644 index 2c3e6b8..0000000 --- a/services/core/src/enumerations/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './auth.enumerations'; -export * from './cloud-dav.enumerations'; -export * from './system-settings.enumerations'; diff --git a/services/core/src/enumerations/system-settings.enumerations.ts b/services/core/src/enumerations/system-settings.enumerations.ts deleted file mode 100644 index 570341f..0000000 --- a/services/core/src/enumerations/system-settings.enumerations.ts +++ /dev/null @@ -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', - } - } -} diff --git a/services/core/src/graphql/system-settings.resolver.ts b/services/core/src/graphql/system-settings.resolver.ts deleted file mode 100644 index 5866799..0000000 --- a/services/core/src/graphql/system-settings.resolver.ts +++ /dev/null @@ -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 { - 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 { - 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; - } -} diff --git a/services/core/src/main.ts b/services/core/src/main.ts deleted file mode 100644 index 13cad38..0000000 --- a/services/core/src/main.ts +++ /dev/null @@ -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(); diff --git a/services/core/src/prisma-post-migrations.ts b/services/core/src/prisma-post-migrations.ts deleted file mode 100644 index a6bc7ad..0000000 --- a/services/core/src/prisma-post-migrations.ts +++ /dev/null @@ -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(); diff --git a/services/core/src/utils/normalize-pairs.ts b/services/core/src/utils/normalize-pairs.ts deleted file mode 100644 index 475acc7..0000000 --- a/services/core/src/utils/normalize-pairs.ts +++ /dev/null @@ -1,58 +0,0 @@ -interface Pairs { - hashKey: string; - hashValue: string; -} - -export const normalizePairs = (pairs: Pairs[]): Record => { - 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): 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:', -}