mirror of
https://github.com/wassname/talk.git
synced 2026-09-12 13:01:11 +08:00
[CORL-155] User Suspending and Banning (#2247)
* feat: suspending, banning, now propogation * feat: adapting to `now` * feat: support auth for suspension/banned * feat: added trace-id to requests * feat: new mutation api with hooks support * feat: added user status filtering, current field * feat: Implement filter by status, adapt to new USER_STATUS type, add lookup helper <3 * fix: typo * fix: tests * chore: rename banned status to ban status * test: feature test + lots of test helper improvements e.g. types * fix: add translation to ban user modal * fix: translation * fix: test
This commit is contained in:
@@ -75,6 +75,9 @@ export async function handleLogout(redis: Redis, req: Request, res: Response) {
|
||||
throw new Error("logout requires a token on the request, none was found");
|
||||
}
|
||||
|
||||
// Talk is guarenteed at this point.
|
||||
const { now } = req.talk!;
|
||||
|
||||
// Decode the token.
|
||||
const decoded = jwt.decode(token, {});
|
||||
if (!decoded) {
|
||||
@@ -88,7 +91,7 @@ export async function handleLogout(redis: Redis, req: Request, res: Response) {
|
||||
const { jti, exp }: LogoutToken = validate(LogoutTokenSchema, decoded);
|
||||
|
||||
// Compute the number of seconds that the token will be valid for.
|
||||
const validFor = exp - Date.now() / 1000;
|
||||
const validFor = exp - now.valueOf() / 1000;
|
||||
if (validFor > 0) {
|
||||
// Invalidate the token, the expiry is in the future and it needs to be
|
||||
// revoked.
|
||||
|
||||
@@ -40,7 +40,8 @@ export default class FacebookStrategy extends OAuth2Strategy<
|
||||
protected async findOrCreateUser(
|
||||
tenant: Tenant,
|
||||
integration: Required<GQLFacebookAuthIntegration>,
|
||||
{ id, photos, emails, displayName }: Profile
|
||||
{ id, photos, emails, displayName }: Profile,
|
||||
now = new Date()
|
||||
) {
|
||||
// Create the user profile that will be used to lookup the User.
|
||||
const profile: FacebookProfile = {
|
||||
@@ -71,14 +72,19 @@ export default class FacebookStrategy extends OAuth2Strategy<
|
||||
emailVerified = false;
|
||||
}
|
||||
|
||||
user = await insert(this.mongo, tenant, {
|
||||
username: displayName,
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
email,
|
||||
emailVerified,
|
||||
avatar,
|
||||
profiles: [profile],
|
||||
});
|
||||
user = await insert(
|
||||
this.mongo,
|
||||
tenant,
|
||||
{
|
||||
username: displayName,
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
email,
|
||||
emailVerified,
|
||||
avatar,
|
||||
profiles: [profile],
|
||||
},
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: maybe update user details?
|
||||
|
||||
@@ -39,7 +39,8 @@ export default class GoogleStrategy extends OAuth2Strategy<
|
||||
protected async findOrCreateUser(
|
||||
tenant: Tenant,
|
||||
integration: Required<GQLGoogleAuthIntegration>,
|
||||
{ id, photos, emails, displayName }: Profile
|
||||
{ id, photos, emails, displayName }: Profile,
|
||||
now = new Date()
|
||||
) {
|
||||
// Create the user profile that will be used to lookup the User.
|
||||
const profile: GoogleProfile = {
|
||||
@@ -70,14 +71,19 @@ export default class GoogleStrategy extends OAuth2Strategy<
|
||||
emailVerified = false;
|
||||
}
|
||||
|
||||
user = await insert(this.mongo, tenant, {
|
||||
username: displayName,
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
email,
|
||||
emailVerified,
|
||||
avatar,
|
||||
profiles: [profile],
|
||||
});
|
||||
user = await insert(
|
||||
this.mongo,
|
||||
tenant,
|
||||
{
|
||||
username: displayName,
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
email,
|
||||
emailVerified,
|
||||
avatar,
|
||||
profiles: [profile],
|
||||
},
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: maybe update user details?
|
||||
|
||||
@@ -33,7 +33,8 @@ export interface Verifier<T = Token> {
|
||||
verify: (
|
||||
tokenString: string,
|
||||
token: T,
|
||||
tenant: Tenant
|
||||
tenant: Tenant,
|
||||
now: Date
|
||||
) => Promise<Readonly<User> | null>;
|
||||
|
||||
/**
|
||||
@@ -58,7 +59,7 @@ export class JWTStrategy extends Strategy {
|
||||
];
|
||||
}
|
||||
|
||||
private async verify(tokenString: string, tenant: Tenant) {
|
||||
private async verify(tokenString: string, tenant: Tenant, now = new Date()) {
|
||||
const token: Token = jwt.decode(tokenString);
|
||||
if (!token || typeof token === "string") {
|
||||
throw new TokenInvalidError(tokenString, "token could not be decoded");
|
||||
@@ -67,7 +68,7 @@ export class JWTStrategy extends Strategy {
|
||||
// Try to verify the token.
|
||||
for (const verifier of this.verifiers) {
|
||||
if (verifier.supports(token, tenant)) {
|
||||
return verifier.verify(tokenString, token, tenant);
|
||||
return verifier.verify(tokenString, token, tenant, now);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,13 +88,13 @@ export class JWTStrategy extends Strategy {
|
||||
return this.pass();
|
||||
}
|
||||
|
||||
const { tenant } = req.talk!;
|
||||
const { now, tenant } = req.talk!;
|
||||
if (!tenant) {
|
||||
return this.error(new TenantNotFoundError(req.hostname));
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await this.verify(token, tenant);
|
||||
const user = await this.verify(token, tenant, now);
|
||||
if (!user) {
|
||||
return this.pass();
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@ export default abstract class OAuth2Strategy<
|
||||
protected abstract findOrCreateUser(
|
||||
tenant: Tenant,
|
||||
integration: Required<T>,
|
||||
profile: Profile
|
||||
profile: Profile,
|
||||
now: Date
|
||||
): Promise<User | undefined>;
|
||||
|
||||
protected verifyCallback = async (
|
||||
@@ -70,6 +71,7 @@ export default abstract class OAuth2Strategy<
|
||||
try {
|
||||
// Talk is defined at this point.
|
||||
const tenant = req.talk!.tenant!;
|
||||
const now = req.talk!.now;
|
||||
|
||||
// Get the integration.
|
||||
const integration = this.getIntegration(tenant.auth.integrations);
|
||||
@@ -78,7 +80,8 @@ export default abstract class OAuth2Strategy<
|
||||
const user = await this.findOrCreateUser(
|
||||
tenant,
|
||||
integration as Required<T>,
|
||||
profile
|
||||
profile,
|
||||
now
|
||||
);
|
||||
|
||||
return done(null, user);
|
||||
|
||||
@@ -137,7 +137,8 @@ export async function findOrCreateOIDCUser(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
integration: OIDCAuthIntegration,
|
||||
token: OIDCIDToken
|
||||
token: OIDCIDToken,
|
||||
now = new Date()
|
||||
): Promise<Readonly<User> | null> {
|
||||
// Unpack/validate the token content.
|
||||
const {
|
||||
@@ -174,14 +175,19 @@ export async function findOrCreateOIDCUser(
|
||||
const username = preferred_username || nickname || name;
|
||||
|
||||
// Create the new user, as one didn't exist before!
|
||||
user = await insert(mongo, tenant, {
|
||||
username,
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
email,
|
||||
emailVerified: email_verified,
|
||||
avatar: picture,
|
||||
profiles: [profile],
|
||||
});
|
||||
user = await insert(
|
||||
mongo,
|
||||
tenant,
|
||||
{
|
||||
username,
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
email,
|
||||
emailVerified: email_verified,
|
||||
avatar: picture,
|
||||
profiles: [profile],
|
||||
},
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: (wyattjoh) possibly update the user profile if the remaining details mismatch?
|
||||
@@ -194,7 +200,8 @@ export function findOrCreateOIDCUserWithToken(
|
||||
tenant: Tenant,
|
||||
client: JwksClient,
|
||||
integration: OIDCAuthIntegration,
|
||||
token: string
|
||||
token: string,
|
||||
now: Date
|
||||
) {
|
||||
return new Promise<Readonly<User> | null>((resolve, reject) => {
|
||||
logger.trace({ tenantID: tenant.id }, "verifying oidc id_token");
|
||||
@@ -219,7 +226,8 @@ export function findOrCreateOIDCUserWithToken(
|
||||
mongo,
|
||||
tenant,
|
||||
integration,
|
||||
decoded as OIDCIDToken
|
||||
decoded as OIDCIDToken,
|
||||
now
|
||||
);
|
||||
return resolve(user);
|
||||
} catch (err) {
|
||||
@@ -303,8 +311,9 @@ export default class OIDCStrategy extends Strategy {
|
||||
return done(new Error("no id_token in params"));
|
||||
}
|
||||
|
||||
// Grab the tenant out of the request, as we need some more details.
|
||||
const { tenant } = req.talk!;
|
||||
// Grab the tenant out of the request, as we need some more details. Talk
|
||||
// is guaranteed at this point.
|
||||
const { now, tenant } = req.talk!;
|
||||
if (!tenant) {
|
||||
// TODO: return a better error.
|
||||
return done(new Error("tenant not found"));
|
||||
@@ -330,7 +339,8 @@ export default class OIDCStrategy extends Strategy {
|
||||
tenant,
|
||||
client,
|
||||
integration,
|
||||
id_token
|
||||
id_token,
|
||||
now
|
||||
);
|
||||
return done(null, user || undefined);
|
||||
} catch (err) {
|
||||
|
||||
@@ -30,7 +30,12 @@ export class OIDCVerifier implements Verifier<OIDCIDToken> {
|
||||
this.cache = new TenantCacheAdapter(tenantCache);
|
||||
}
|
||||
|
||||
public async verify(tokenString: string, token: OIDCIDToken, tenant: Tenant) {
|
||||
public async verify(
|
||||
tokenString: string,
|
||||
token: OIDCIDToken,
|
||||
tenant: Tenant,
|
||||
now: Date
|
||||
) {
|
||||
// Ensure that the integration is enabled.
|
||||
const integration = getEnabledIntegration(tenant.auth.integrations.oidc);
|
||||
|
||||
@@ -52,7 +57,8 @@ export class OIDCVerifier implements Verifier<OIDCIDToken> {
|
||||
tenant,
|
||||
client,
|
||||
integration,
|
||||
tokenString
|
||||
tokenString,
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,8 @@ export async function findOrCreateSSOUser(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
integration: GQLSSOAuthIntegration,
|
||||
token: SSOToken
|
||||
token: SSOToken,
|
||||
now = new Date()
|
||||
) {
|
||||
if (!token.user) {
|
||||
// TODO: (wyattjoh) replace with better error.
|
||||
@@ -71,13 +72,18 @@ export async function findOrCreateSSOUser(
|
||||
// FIXME: (wyattjoh) implement rules! Not all users should be able to create an account via this method.
|
||||
|
||||
// Create the new user, as one didn't exist before!
|
||||
user = await insert(mongo, tenant, {
|
||||
username,
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
email,
|
||||
avatar,
|
||||
profiles: [profile],
|
||||
});
|
||||
user = await insert(
|
||||
mongo,
|
||||
tenant,
|
||||
{
|
||||
username,
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
email,
|
||||
avatar,
|
||||
profiles: [profile],
|
||||
},
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: (wyattjoh) possibly update the user profile if the remaining details mismatch?
|
||||
@@ -122,7 +128,12 @@ export class SSOVerifier implements Verifier<SSOToken> {
|
||||
return tenant.auth.integrations.sso.enabled && isSSOToken(token);
|
||||
}
|
||||
|
||||
public async verify(tokenString: string, token: SSOToken, tenant: Tenant) {
|
||||
public async verify(
|
||||
tokenString: string,
|
||||
token: SSOToken,
|
||||
tenant: Tenant,
|
||||
now = new Date()
|
||||
) {
|
||||
const integration = tenant.auth.integrations.sso;
|
||||
if (!integration.enabled) {
|
||||
// TODO: (wyattjoh) return a better error.
|
||||
@@ -140,6 +151,6 @@ export class SSOVerifier implements Verifier<SSOToken> {
|
||||
algorithms: ["HS256"], // TODO: (wyattjoh) investigate replacing algorithm.
|
||||
});
|
||||
|
||||
return findOrCreateSSOUser(this.mongo, tenant, integration, token);
|
||||
return findOrCreateSSOUser(this.mongo, tenant, integration, token, now);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import uuid from "uuid/v1";
|
||||
|
||||
import { TenantNotFoundError } from "talk-server/errors";
|
||||
import TenantCache from "talk-server/services/tenant/cache";
|
||||
import { RequestHandler } from "talk-server/types/express";
|
||||
@@ -12,9 +14,18 @@ export const tenantMiddleware = ({
|
||||
passNoTenant = false,
|
||||
}: MiddlewareOptions): RequestHandler => async (req, res, next) => {
|
||||
try {
|
||||
// Set Talk on the request.
|
||||
if (!req.talk) {
|
||||
req.talk = {};
|
||||
const id = uuid();
|
||||
|
||||
// Write the ID on the request.
|
||||
res.set("X-Trace-ID", id);
|
||||
|
||||
// The only call to `new Date()` as a part of the request process. This
|
||||
// is passed around the request to ensure constant-time actions.
|
||||
const now = new Date();
|
||||
|
||||
// Set Talk on the request.
|
||||
req.talk = { id, now };
|
||||
}
|
||||
|
||||
// Set the Talk Tenant Cache on the request.
|
||||
|
||||
Reference in New Issue
Block a user