feat: initial local passport strategy

This commit is contained in:
Wyatt Johnson
2018-07-05 15:58:19 -06:00
parent 3e90877012
commit 87ce65755c
15 changed files with 312 additions and 217 deletions
+30 -77
View File
@@ -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",
+5 -1
View File
@@ -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"
}
}
}
+1 -3
View File
@@ -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,
});
}
@@ -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;
}
@@ -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)
);
}
+98 -35
View File
@@ -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<string, StrategyItem>;
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 });
}
+9
View File
@@ -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;
}
+1 -2
View File
@@ -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);
@@ -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
),
});
@@ -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<string, User | null>(ids =>
retrieveMany(ctx.db, ctx.tenant.id, ids)
retrieveManyUsers(ctx.db, ctx.tenant.id, ids)
),
});
@@ -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.
"""
+11 -9
View File
@@ -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<string, any>;
}
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,
+1 -1
View File
@@ -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 });
}
+30 -35
View File
@@ -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<T> {
status: T; // TODO: migrate field
assigned_by?: string;
@@ -61,7 +40,7 @@ export interface UserStatusItem<T> {
}
export interface UserStatus {
username: UserStatusItem<UserUsernameStatus>;
username: UserStatusItem<GQLUSER_USERNAME_STATUS>;
banned: UserStatusItem<boolean>;
suspension: UserStatusItem<Date | null>;
}
@@ -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;
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { Omit } from "talk-common/types";
import {
Comment,
CommentStatus,
create as createComment,
createComment,
CreateCommentInput,
} from "talk-server/models/comment";