mirror of
https://github.com/wassname/talk.git
synced 2026-08-01 13:00:55 +08:00
feat: initial local passport strategy
This commit is contained in:
Generated
+30
-77
@@ -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
@@ -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
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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,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.
|
||||
"""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Omit } from "talk-common/types";
|
||||
import {
|
||||
Comment,
|
||||
CommentStatus,
|
||||
create as createComment,
|
||||
createComment,
|
||||
CreateCommentInput,
|
||||
} from "talk-server/models/comment";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user