diff --git a/package-lock.json b/package-lock.json index 4d7d90884..6b61ad34c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1403,6 +1403,12 @@ "lodash.deburr": "^4.1.0" } }, + "@types/bcryptjs": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.1.tgz", + "integrity": "sha512-CVJ8ExtzUQJzLJbEk/lWrHD3MTvstTodjWidcH23gCii5WSD0z1TPSLqSdtbn5eCDw+DxfKgoUALi+loe8ftXA==", + "dev": true + }, "@types/bluebird": { "version": "3.5.20", "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.20.tgz", @@ -1624,6 +1630,17 @@ "@types/express": "*" } }, + "@types/passport-local": { + "version": "1.0.33", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.33.tgz", + "integrity": "sha512-+rn6ZIxje0jZ2+DAiWFI8vGG7ZFKB0hXx2cUdMmudSWsigSq6ES7Emso46r4HJk0qCgrZVfI8sJiM7HIYf4SbA==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, "@types/passport-oauth2": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.5.tgz", @@ -4380,6 +4397,11 @@ "tweetnacl": "^0.14.3" } }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" + }, "big.js": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", @@ -9865,12 +9887,6 @@ "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", "dev": true }, - "ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", - "dev": true - }, "immutable": { "version": "3.7.6", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", @@ -12946,50 +12962,6 @@ "which": "^1.3.0" } }, - "nodemon": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.17.5.tgz", - "integrity": "sha512-FG2mWJU1Y58a9ktgMJ/RZpsiPz3b7ge77t/okZHEa4NbrlXGKZ8s1A6Q+C7+JPXohAfcPALRwvxcAn8S874pmw==", - "dev": true, - "requires": { - "chokidar": "^2.0.2", - "debug": "^3.1.0", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.0", - "semver": "^5.5.0", - "supports-color": "^5.2.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.2", - "update-notifier": "^2.3.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "nopt": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", @@ -13529,6 +13501,14 @@ "pause": "0.0.1" } }, + "passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", + "requires": { + "passport-strategy": "1.x.x" + } + }, "passport-oauth2": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.4.0.tgz", @@ -16106,15 +16086,6 @@ "integrity": "sha512-+AqO1Ae+N/4r7Rvchrdm432afjT9hqJRyBN3DQv9At0tPz4hIFSGKbq64fN9dVoCow4oggIIax5/iONx0r9hZw==", "dev": true }, - "pstree.remy": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.0.tgz", - "integrity": "sha512-q5I5vLRMVtdWa8n/3UEzZX7Lfghzrg9eG2IKk2ENLSofKRCXVqMvMUHxCKgXNaqH/8ebhBxrqftHWnyTFweJ5Q==", - "dev": true, - "requires": { - "ps-tree": "^1.1.0" - } - }, "public-encrypt": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", @@ -18966,15 +18937,6 @@ "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", "dev": true }, - "touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "requires": { - "nopt": "~1.0.10" - } - }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", @@ -19852,15 +19814,6 @@ "integrity": "sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==", "dev": true }, - "undefsafe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", - "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", - "dev": true, - "requires": { - "debug": "^2.2.0" - } - }, "unherit": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.1.tgz", diff --git a/package.json b/package.json index cbe872d79..76e1cb87f 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "license": "Apache-2.0", "dependencies": { "apollo-server-express": "^1.3.6", + "bcryptjs": "^2.4.3", "bunyan": "^1.8.12", "convict": "^4.3.0", "dataloader": "^1.4.0", @@ -46,6 +47,7 @@ "luxon": "^1.2.1", "mongodb": "^3.0.10", "passport": "^0.4.0", + "passport-local": "^1.0.0", "passport-oauth2": "^1.4.0", "passport-strategy": "^1.0.0", "performance-now": "^2.1.0", @@ -58,6 +60,7 @@ "@babel/polyfill": "7.0.0-beta.49", "@babel/preset-env": "7.0.0-beta.49", "@babel/preset-react": "7.0.0-beta.49", + "@types/bcryptjs": "^2.4.1", "@types/bunyan": "^1.8.4", "@types/chokidar": "^1.7.5", "@types/classnames": "^2.2.4", @@ -76,6 +79,7 @@ "@types/mongodb": "^3.0.19", "@types/node": "^10.3.1", "@types/passport": "^0.4.5", + "@types/passport-local": "^1.0.33", "@types/passport-oauth2": "^1.4.5", "@types/passport-strategy": "^0.2.33", "@types/query-string": "^6.1.0", @@ -149,4 +153,4 @@ "webpack-hot-client": "^4.0.3", "webpack-manifest-plugin": "^2.0.3" } -} \ No newline at end of file +} diff --git a/scripts/types.js b/scripts/types.js index 0bf026bd8..be1449842 100644 --- a/scripts/types.js +++ b/scripts/types.js @@ -72,9 +72,7 @@ async function main() { file.types = await generateTSTypesAsString(schema, { tabSpaces: 2, typePrefix: "GQL", - contextType: "any", - strictNulls: false, - minimizeInterfaceImplementation: true, + strictNulls: true, ...file.config, }); } diff --git a/src/core/server/app/middleware/passport/index.ts b/src/core/server/app/middleware/passport/index.ts index bebc52631..1b6968564 100644 --- a/src/core/server/app/middleware/passport/index.ts +++ b/src/core/server/app/middleware/passport/index.ts @@ -1,63 +1,33 @@ +import { NextFunction, Response } from "express"; import { Db } from "mongodb"; import passport, { Authenticator } from "passport"; -import { NextFunction, Response } from "express"; -import OIDCStrategy, { - Token, - VerifyCallback, -} from "talk-server/app/middleware/passport/oidc"; -import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types"; -import { Tenant } from "talk-server/models/tenant"; -import { create, retrieveWithProfile, User } from "talk-server/models/user"; +import { createLocalStrategy } from "talk-server/app/middleware/passport/local"; +import { createOIDCStrategy } from "talk-server/app/middleware/passport/oidc"; +import { User } from "talk-server/models/user"; import { Request } from "talk-server/types/express"; +export type VerifyCallback = ( + err?: Error | null, + user?: User | null, + info?: object +) => void; + export interface PassportOptions { db: Db; } -async function verifyOIDC( - db: Db, - tenant: Tenant, - { iss, sub, email, email_verified }: Token, - done: VerifyCallback -) { - try { - // Construct the profile that will be used to query for the user. - const profile = { - type: "oidc", - provider: iss, - id: sub, - }; - - // Try to lookup user given their id provided in the `sub` claim. - let user = await retrieveWithProfile(db, tenant.id, profile); - if (!user) { - // FIXME: implement rules. - - // Create the new user, as one didn't exist before! - user = await create(db, tenant.id, { - username: null, - role: GQLUSER_ROLE.COMMENTER, - email, - email_verified, - profiles: [profile], - }); - } - - return done(null, user); - } catch (err) { - return done(err); - } -} - export function createPassport({ db, }: PassportOptions): passport.Authenticator { // Create the authenticator. const auth = new Authenticator(); - // Process the OIDC Strategy. - auth.use(new OIDCStrategy({ db }, verifyOIDC.bind(null, db))); + // Use the OIDC Strategy. + auth.use(createOIDCStrategy({ db })); + + // Use the LocalStrategy. + auth.use(createLocalStrategy({ db })); return auth; } diff --git a/src/core/server/app/middleware/passport/local.ts b/src/core/server/app/middleware/passport/local.ts new file mode 100644 index 000000000..4aa707153 --- /dev/null +++ b/src/core/server/app/middleware/passport/local.ts @@ -0,0 +1,61 @@ +import { Db } from "mongodb"; +import { Strategy as LocalStrategy } from "passport-local"; + +import { VerifyCallback } from "talk-server/app/middleware/passport"; +import { + retrieveUserWithProfile, + verifyUserPassword, +} from "talk-server/models/user"; +import { Request } from "talk-server/types/express"; + +async function verify( + db: Db, + req: Request, + email: string, + password: string, + done: VerifyCallback +) { + try { + // The tenant is guaranteed at this point. + const tenant = req.tenant!; + + // TODO: rate limit the ip address + + // Get the user from the database. + const user = await retrieveUserWithProfile(db, tenant.id, { + id: email, + type: "local", + }); + if (!user) { + // The user didn't exist. + return done(null, null); + } + + // Verify the password. + const passwordVerified = await verifyUserPassword(user, password); + if (!passwordVerified) { + // TODO: return better error + return done(new Error("invalid password")); + } + + return done(null, user); + } catch (err) { + return done(err); + } +} + +export interface LocalStrategyOptions { + db: Db; +} + +export function createLocalStrategy({ db }: LocalStrategyOptions) { + return new LocalStrategy( + { + usernameField: "email", + passwordField: "password", + session: false, + passReqToCallback: true, + }, + verify.bind(null, db) + ); +} diff --git a/src/core/server/app/middleware/passport/oidc.ts b/src/core/server/app/middleware/passport/oidc.ts index ceac0bc37..f75fe43b3 100644 --- a/src/core/server/app/middleware/passport/oidc.ts +++ b/src/core/server/app/middleware/passport/oidc.ts @@ -1,57 +1,106 @@ import jwt from "jsonwebtoken"; import jwks, { JwksClient } from "jwks-rsa"; +import { Db } from "mongodb"; import { Strategy as OAuth2Strategy } from "passport-oauth2"; import { Strategy } from "passport-strategy"; import { reconstructURL } from "talk-server/app/url"; +import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types"; import { OIDCAuthIntegration, Tenant } from "talk-server/models/tenant"; -import { User } from "talk-server/models/user"; +import { createUser, retrieveUserWithProfile } from "talk-server/models/user"; import { Request } from "talk-server/types/express"; -export type OIDCStrategyOptions = any; +import { VerifyCallback } from "./index"; export interface Params { id_token?: string; } -export type VerifyCallback = ( - err?: Error | null, - user?: User | null, - info?: object -) => void; - -export interface Token { +export interface OIDCIDToken { iss: string; sub: string; email: string; email_verified?: boolean; } -export type OIDCStrategyCallback = ( - tenant: Tenant, - token: Token, - done: VerifyCallback -) => void; - export interface StrategyItem { strategy: OAuth2Strategy; jwksClient?: JwksClient; } +export interface OIDCStrategyOptions { + db: Db; +} + +export function isOIDCToken(token: OIDCIDToken | object): token is OIDCIDToken { + if ( + (token as OIDCIDToken).iss && + (token as OIDCIDToken).sub && + (token as OIDCIDToken).email + ) { + return true; + } + + return false; +} + +export async function findOrCreateOIDCUser( + db: Db, + tenant: Tenant, + { iss, sub, email, email_verified }: OIDCIDToken +) { + // Construct the profile that will be used to query for the user. + const profile = { + type: "oidc", + provider: iss, + id: sub, + }; + + // Try to lookup user given their id provided in the `sub` claim. + let user = await retrieveUserWithProfile(db, tenant.id, profile); + if (!user) { + // FIXME: implement rules. + + // Create the new user, as one didn't exist before! + user = await createUser(db, tenant.id, { + username: null, + role: GQLUSER_ROLE.COMMENTER, + email, + email_verified, + profiles: [profile], + }); + } + + return user; +} + // FIXME: attach strategy to cache updates of the tenants export default class OIDCStrategy extends Strategy { public name: string; - private verify: OIDCStrategyCallback; + private db: Db; private cache: Map; - constructor(options: OIDCStrategyOptions, verify: OIDCStrategyCallback) { + constructor({ db }: OIDCStrategyOptions) { super(); this.name = "oidc"; this.cache = new Map(); - this.verify = verify; + this.db = db; + } + + private async verify( + tenant: Tenant, + token: OIDCIDToken, + done: VerifyCallback + ) { + try { + const user = await findOrCreateOIDCUser(this.db, tenant, token); + return done(null, user); + } catch (err) { + return done(err); + } } private lookupJWKSClient( @@ -139,7 +188,7 @@ export default class OIDCStrategy extends Strategy { return done(err); } - this.verify(tenant!, decoded as Token, done); + this.verify(tenant!, decoded as OIDCIDToken, done); } ); } @@ -183,6 +232,12 @@ export default class OIDCStrategy extends Strategy { throw new Error("integration not found"); } + // Handle when the integration is enabled/disabled. + if (!integration.enabled) { + // TODO: return a better error. + throw new Error("integration not enabled"); + } + // Try to get the Tenant's cached integrations. let entry = this.cache.get(tenant.id); if (!entry) { @@ -202,22 +257,30 @@ export default class OIDCStrategy extends Strategy { } public async authenticate(req: Request) { - // Lookup the strategy. - const strategy = await this.lookupStrategy(req); - if (!strategy) { - return; + try { + // Lookup the strategy. + const strategy = await this.lookupStrategy(req); + if (!strategy) { + throw new Error("strategy not found"); + } + + // Augment the strategy with the request method bindings. + strategy.error = this.error.bind(this); + strategy.fail = this.fail.bind(this); + strategy.pass = this.pass.bind(this); + strategy.redirect = this.redirect.bind(this); + strategy.success = this.success.bind(this); + + // Authenticate with the strategy, binding the current context to the method + // to provide it with the augmented passport handlers. We also request the + // 'openid' scope so we can get an id_token back. + strategy.authenticate(req, { scope: "openid email", session: false }); + } catch (err) { + return this.error(err); } - - // Augment the strategy with the request method bindings. - strategy.error = this.error.bind(this); - strategy.fail = this.fail.bind(this); - strategy.pass = this.pass.bind(this); - strategy.redirect = this.redirect.bind(this); - strategy.success = this.success.bind(this); - - // Authenticate with the strategy, binding the current context to the method - // to provide it with the augmented passport handlers. We also request the - // 'openid' scope so we can get an id_token back. - strategy.authenticate(req, { scope: "openid email", session: false }); } } + +export function createOIDCStrategy({ db }: OIDCStrategyOptions) { + return new OIDCStrategy({ db }); +} diff --git a/src/core/server/app/router.ts b/src/core/server/app/router.ts index 1ae9934b8..a1a668b51 100644 --- a/src/core/server/app/router.ts +++ b/src/core/server/app/router.ts @@ -33,6 +33,11 @@ async function createTenantRouter(app: AppOptions, options: RouterOptions) { router.use(tenantMiddleware({ db: app.mongo })); router.use(options.passport.initialize()); + router.use( + "/auth/local", + express.json(), + authenticate(options.passport, "local") + ); router.use("/auth/oidc", authenticate(options.passport, "oidc")); router.use("/auth/oidc/callback", authenticate(options.passport, "oidc")); // router.use("/auth/google", options.passport.authenticate("google")); @@ -64,6 +69,10 @@ async function createAPIRouter(app: AppOptions, options: RouterOptions) { } export interface RouterOptions { + /** + * passport is the instance of the Authenticator that can be used to create + * and mount new authentication middleware. + */ passport: passport.Authenticator; } diff --git a/src/core/server/app/url.ts b/src/core/server/app/url.ts index b9f533f1a..b656e4421 100644 --- a/src/core/server/app/url.ts +++ b/src/core/server/app/url.ts @@ -1,11 +1,10 @@ import { Request } from "talk-server/types/express"; import { URL } from "url"; -export function reconstructURL(req: Request, input?: string): string { +export function reconstructURL(req: Request, path: string = "/"): string { const scheme = req.secure ? "https" : "http"; const host = req.get("host"); const base = `${scheme}://${host}`; - const path = input || req.originalUrl; const url = new URL(path, base); diff --git a/src/core/server/graph/tenant/loaders/comments.ts b/src/core/server/graph/tenant/loaders/comments.ts index 0e68876e2..765ed6768 100644 --- a/src/core/server/graph/tenant/loaders/comments.ts +++ b/src/core/server/graph/tenant/loaders/comments.ts @@ -6,17 +6,23 @@ import { CommentToRepliesArgs, } from "talk-server/graph/tenant/schema/__generated__/types"; import { - retrieveAssetConnection, - retrieveMany, - retrieveRepliesConnection, + retrieveCommentAssetConnection, + retrieveManyComments, + retrieveCommentRepliesConnection, } from "talk-server/models/comment"; export default (ctx: Context) => ({ comment: new DataLoader((ids: string[]) => - retrieveMany(ctx.db, ctx.tenant.id, ids) + retrieveManyComments(ctx.db, ctx.tenant.id, ids) ), forAsset: (assetID: string, input: AssetToCommentsArgs) => - retrieveAssetConnection(ctx.db, ctx.tenant.id, assetID, input), + retrieveCommentAssetConnection(ctx.db, ctx.tenant.id, assetID, input), forParent: (assetID: string, parentID: string, input: CommentToRepliesArgs) => - retrieveRepliesConnection(ctx.db, ctx.tenant.id, assetID, parentID, input), + retrieveCommentRepliesConnection( + ctx.db, + ctx.tenant.id, + assetID, + parentID, + input + ), }); diff --git a/src/core/server/graph/tenant/loaders/users.ts b/src/core/server/graph/tenant/loaders/users.ts index d875bc6b4..34bbebc10 100644 --- a/src/core/server/graph/tenant/loaders/users.ts +++ b/src/core/server/graph/tenant/loaders/users.ts @@ -1,9 +1,9 @@ import DataLoader from "dataloader"; import Context from "talk-server/graph/tenant/context"; -import { retrieveMany, User } from "talk-server/models/user"; +import { retrieveManyUsers, User } from "talk-server/models/user"; export default (ctx: Context) => ({ user: new DataLoader(ids => - retrieveMany(ctx.db, ctx.tenant.id, ids) + retrieveManyUsers(ctx.db, ctx.tenant.id, ids) ), }); diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index bf6e5d6f8..2e3ae43af 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -284,6 +284,41 @@ enum USER_ROLE { ADMIN } +enum USER_USERNAME_STATUS { + """ + UNSET is used when the username can be changed, and does not necessarily + require moderator action to become active. This can be used when the user + signs up with a social login and has the option of setting their own + username. + """ + UNSET + + """ + SET is used when the username has been set for the first time, but cannot + change without the username being rejected by a moderator and that moderator + agreeing that the username should be allowed to change. + """ + SET + + """ + APPROVED is used when the username was changed, and subsequently approved by + said moderator. + """ + APPROVED + + """ + REJECTED is used when the username was changed, and subsequently rejected by + said moderator. + """ + REJECTED + + """ + CHANGED is used after a user has changed their username after it was + rejected. + """ + CHANGED +} + """ User is someone that leaves Comments, and logs in. """ diff --git a/src/core/server/models/comment.ts b/src/core/server/models/comment.ts index 5c0726669..df5d1f877 100644 --- a/src/core/server/models/comment.ts +++ b/src/core/server/models/comment.ts @@ -34,7 +34,7 @@ export enum CommentStatus { export interface Comment extends TenantResource { readonly id: string; - parent_id?: string; + parent_id: string | null; author_id: string; asset_id: string; body: string; @@ -45,9 +45,7 @@ export interface Comment extends TenantResource { reply_count: number; created_at: Date; deleted_at?: Date; - metadata?: { - [_: string]: any; - }; + metadata?: Record; } export type CreateCommentInput = Omit< @@ -60,7 +58,7 @@ export type CreateCommentInput = Omit< | "status_history" >; -export async function create( +export async function createComment( db: Db, tenantID: string, input: CreateCommentInput @@ -106,11 +104,15 @@ export async function create( return comment; } -export async function retrieve(db: Db, tenantID: string, id: string) { +export async function retrieveComment(db: Db, tenantID: string, id: string) { return collection(db).findOne({ id, tenant_id: tenantID }); } -export async function retrieveMany(db: Db, tenantID: string, ids: string[]) { +export async function retrieveManyComments( + db: Db, + tenantID: string, + ids: string[] +) { const cursor = await collection(db).find({ id: { $in: ids, @@ -164,7 +166,7 @@ function nodesToEdge(input: ConnectionInput, nodes: Comment[]) { * @param parentID the parent id for the comment to retrieve * @param input connection configuration */ -export async function retrieveRepliesConnection( +export async function retrieveCommentRepliesConnection( db: Db, tenantID: string, assetID: string, @@ -190,7 +192,7 @@ export async function retrieveRepliesConnection( * @param assetID the Asset id for the comment to retrieve * @param input connection configuration */ -export async function retrieveAssetConnection( +export async function retrieveCommentAssetConnection( db: Db, tenantID: string, assetID: string, diff --git a/src/core/server/models/tenant.ts b/src/core/server/models/tenant.ts index 123c6b261..e18ab7ec0 100644 --- a/src/core/server/models/tenant.ts +++ b/src/core/server/models/tenant.ts @@ -210,7 +210,7 @@ export async function retrieveTenantByDomain(db: Db, domain: string) { return collection(db).findOne({ domain }); } -export async function retrieve(db: Db, id: string) { +export async function retrieveTenant(db: Db, id: string) { return collection(db).findOne({ id }); } diff --git a/src/core/server/models/user.ts b/src/core/server/models/user.ts index 487f7174f..1fbcbb031 100644 --- a/src/core/server/models/user.ts +++ b/src/core/server/models/user.ts @@ -1,9 +1,13 @@ +import bcrypt from "bcryptjs"; import { merge } from "lodash"; import { Db } from "mongodb"; import uuid from "uuid"; import { Omit, Sub } from "talk-common/types"; -import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types"; +import { + GQLUSER_ROLE, + GQLUSER_USERNAME_STATUS, +} from "talk-server/graph/tenant/schema/__generated__/types"; import { ActionCounts } from "talk-server/models/actions"; import { TenantResource } from "talk-server/models/tenant"; @@ -14,7 +18,7 @@ function collection(db: Db) { export interface Profile { readonly id: string; readonly type: string; - provider: string; + provider?: string; } export interface Token { @@ -23,31 +27,6 @@ export interface Token { active: boolean; } -export enum UserUsernameStatus { - // UNSET is used when the username can be changed, and does not necessarily - // require moderator action to become active. This can be used when the user - // signs up with a social login and has the option of setting their own - // username. - UNSET = "UNSET", - - // SET is used when the username has been set for the first time, but cannot - // change without the username being rejected by a moderator and that moderator - // agreeing that the username should be allowed to change. - SET = "SET", - - // APPROVED is used when the username was changed, and subsequently approved by - // said moderator. - APPROVED = "APPROVED", - - // REJECTED is used when the username was changed, and subsequently rejected by - // said moderator. - REJECTED = "REJECTED", - - // CHANGED is used after a user has changed their username after it was - // rejected. - CHANGED = "CHANGED", -} - export interface UserStatusHistory { status: T; // TODO: migrate field assigned_by?: string; @@ -61,7 +40,7 @@ export interface UserStatusItem { } export interface UserStatus { - username: UserStatusItem; + username: UserStatusItem; banned: UserStatusItem; suspension: UserStatusItem; } @@ -92,7 +71,11 @@ export type CreateUserInput = Omit< | "created_at" >; -export async function create(db: Db, tenantID: string, input: CreateUserInput) { +export async function createUser( + db: Db, + tenantID: string, + input: CreateUserInput +) { const now = new Date(); // default are the properties set by the application when a new user is @@ -114,8 +97,8 @@ export async function create(db: Db, tenantID: string, input: CreateUserInput) { }, username: { status: input.username - ? UserUsernameStatus.SET - : UserUsernameStatus.UNSET, + ? GQLUSER_USERNAME_STATUS.SET + : GQLUSER_USERNAME_STATUS.UNSET, history: [], }, }, @@ -131,11 +114,15 @@ export async function create(db: Db, tenantID: string, input: CreateUserInput) { return user; } -export async function retrieve(db: Db, tenantID: string, id: string) { +export async function retrieveUser(db: Db, tenantID: string, id: string) { return collection(db).findOne({ id, tenant_id: tenantID }); } -export async function retrieveMany(db: Db, tenantID: string, ids: string[]) { +export async function retrieveManyUsers( + db: Db, + tenantID: string, + ids: string[] +) { const cursor = await collection(db).find({ id: { $in: ids, @@ -148,7 +135,7 @@ export async function retrieveMany(db: Db, tenantID: string, ids: string[]) { return ids.map(id => users.find(comment => comment.id === id) || null); } -export async function retrieveWithProfile( +export async function retrieveUserWithProfile( db: Db, tenantID: string, profile: Profile @@ -161,7 +148,7 @@ export async function retrieveWithProfile( }); } -export async function updateRole( +export async function updateUserRole( db: Db, tenantID: string, id: string, @@ -175,3 +162,11 @@ export async function updateRole( return result.value || null; } + +export async function verifyUserPassword(user: User, password: string) { + if (user.password) { + return bcrypt.compare(user.password, password); + } + + return false; +} diff --git a/src/core/server/services/comments/index.ts b/src/core/server/services/comments/index.ts index 95a9001cb..f13852dfa 100644 --- a/src/core/server/services/comments/index.ts +++ b/src/core/server/services/comments/index.ts @@ -4,7 +4,7 @@ import { Omit } from "talk-common/types"; import { Comment, CommentStatus, - create as createComment, + createComment, CreateCommentInput, } from "talk-server/models/comment";