mirror of
https://github.com/wassname/talk.git
synced 2026-08-06 13:41:02 +08:00
Merge branch 'next' into permalink
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { User } from "talk-server/models/user";
|
||||
|
||||
export interface CommonContextOptions {
|
||||
user?: User;
|
||||
}
|
||||
|
||||
export default class CommonContext {
|
||||
public user?: User;
|
||||
|
||||
constructor({ user }: CommonContextOptions) {
|
||||
this.user = user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { DirectiveResolverFn } from "graphql-tools";
|
||||
|
||||
import CommonContext from "talk-server/graph/common/context";
|
||||
import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
export interface AuthDirectiveArgs {
|
||||
roles?: GQLUSER_ROLE[];
|
||||
userIDField?: string;
|
||||
}
|
||||
|
||||
const auth: DirectiveResolverFn<
|
||||
Record<string, string | undefined>,
|
||||
CommonContext
|
||||
> = (next, src, { roles, userIDField }: AuthDirectiveArgs, { user }) => {
|
||||
// If there is a user on the request.
|
||||
if (user) {
|
||||
// If the role and user owner checks are disabled, then allow them based on
|
||||
// their authenticated status.
|
||||
if (!roles && !userIDField) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// And the user has the expected role.
|
||||
if (roles && roles.includes(user.role)) {
|
||||
// Let the request continue.
|
||||
return next();
|
||||
}
|
||||
|
||||
// Or the item is owned by the specific user.
|
||||
if (userIDField && src[userIDField] && src[userIDField] === user.id) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: return better error.
|
||||
throw new Error("not authorized");
|
||||
};
|
||||
|
||||
export default auth;
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Kind } from "graphql";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import Cursor from "./cursor";
|
||||
|
||||
describe("parseLiteral", () => {
|
||||
it("parses a date from a string", () => {
|
||||
expect(
|
||||
Cursor.parseLiteral({
|
||||
kind: Kind.STRING,
|
||||
value: "2018-07-16T18:34:26.744Z",
|
||||
})
|
||||
).toBeInstanceOf(Date);
|
||||
|
||||
expect(
|
||||
Cursor.parseLiteral({
|
||||
kind: Kind.STRING,
|
||||
value: "this-should-fail",
|
||||
})
|
||||
).toEqual(null);
|
||||
|
||||
expect(
|
||||
Cursor.parseLiteral({
|
||||
kind: Kind.STRING,
|
||||
value: "",
|
||||
})
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it("parses a number from a string", () => {
|
||||
expect(
|
||||
Cursor.parseLiteral({
|
||||
kind: Kind.STRING,
|
||||
value: "20",
|
||||
})
|
||||
).toEqual(20);
|
||||
|
||||
expect(
|
||||
Cursor.parseLiteral({
|
||||
kind: Kind.STRING,
|
||||
value: "0",
|
||||
})
|
||||
).toEqual(0);
|
||||
|
||||
expect(
|
||||
Cursor.parseLiteral({
|
||||
kind: Kind.STRING,
|
||||
value: "null",
|
||||
})
|
||||
).toEqual(null);
|
||||
|
||||
expect(
|
||||
Cursor.parseLiteral({
|
||||
kind: Kind.STRING,
|
||||
value: "0",
|
||||
})
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it("parses a number from a number", () => {
|
||||
expect(
|
||||
Cursor.parseLiteral({
|
||||
kind: Kind.INT,
|
||||
value: "20",
|
||||
})
|
||||
).toEqual(20);
|
||||
|
||||
expect(
|
||||
Cursor.parseLiteral({
|
||||
kind: Kind.INT,
|
||||
value: "0",
|
||||
})
|
||||
).toEqual(0);
|
||||
|
||||
expect(
|
||||
Cursor.parseLiteral({
|
||||
kind: Kind.INT,
|
||||
value: "",
|
||||
})
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it("does not parse unknown kinds", () => {
|
||||
expect(
|
||||
Cursor.parseLiteral({
|
||||
kind: Kind.FLOAT,
|
||||
value: "0.0",
|
||||
})
|
||||
).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("serialize", () => {
|
||||
it("renders native dates correctly", () => {
|
||||
const date = new Date();
|
||||
const expected = date.toISOString();
|
||||
expect(Cursor.serialize(date)).toEqual(expected);
|
||||
|
||||
expect(Cursor.serialize({})).toEqual(null);
|
||||
});
|
||||
|
||||
it("renders luxon dates correctly", () => {
|
||||
const date = DateTime.fromJSDate(new Date());
|
||||
const expected = date.toISO();
|
||||
expect(Cursor.serialize(date)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("renders numbers correctly", () => {
|
||||
let value = 50;
|
||||
let expected = "50";
|
||||
expect(Cursor.serialize(value)).toEqual(expected);
|
||||
|
||||
value = 0;
|
||||
expected = "0";
|
||||
expect(Cursor.serialize(value)).toEqual(expected);
|
||||
|
||||
expect(Cursor.serialize(null)).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseValue", () => {
|
||||
it("parses the string value of a Date", () => {
|
||||
const date = new Date();
|
||||
const expected = date.toISOString();
|
||||
expect(Cursor.parseValue(expected)).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it("parses the string value of a number", () => {
|
||||
expect(Cursor.parseValue("0")).toEqual(0);
|
||||
});
|
||||
|
||||
it("handles invalid properties", () => {
|
||||
expect(Cursor.parseValue(null)).toEqual(null);
|
||||
expect(Cursor.parseValue(2)).toEqual(null);
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,15 @@
|
||||
import { GraphQLScalarType } from "graphql";
|
||||
import { Kind } from "graphql/language";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import { Cursor } from "talk-server/models/connection";
|
||||
|
||||
function parseIntegerCursor(value: string): number | null {
|
||||
try {
|
||||
const cursor = parseInt(value, 10);
|
||||
if (isNaN(cursor)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return cursor;
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Db } from "mongodb";
|
||||
import CommonContext from "talk-server/graph/common/context";
|
||||
|
||||
export interface ManagementContextOptions {
|
||||
db: Db;
|
||||
}
|
||||
|
||||
export default class ManagementContext {
|
||||
export default class ManagementContext extends CommonContext {
|
||||
public db: Db;
|
||||
|
||||
constructor({ db }: ManagementContextOptions) {
|
||||
super({});
|
||||
|
||||
this.db = db;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Cursor from "../../common/scalars/cursor";
|
||||
import { GQLResolver } from "talk-server/graph/management/schema/__generated__/types";
|
||||
|
||||
export default {
|
||||
Cursor,
|
||||
};
|
||||
const Resolvers: GQLResolver = {};
|
||||
|
||||
export default Resolvers;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { IResolvers } from "graphql-tools";
|
||||
|
||||
import { loadSchema } from "talk-common/graphql";
|
||||
import resolvers from "talk-server/graph/management/resolvers";
|
||||
|
||||
export default function getManagementSchema() {
|
||||
return loadSchema("management", resolvers);
|
||||
return loadSchema("management", resolvers as IResolvers);
|
||||
}
|
||||
|
||||
@@ -7,27 +7,22 @@ Time represented as an ISO8601 string.
|
||||
"""
|
||||
scalar Time
|
||||
|
||||
"""
|
||||
Cursor represents a paginating cursor.
|
||||
"""
|
||||
scalar Cursor
|
||||
|
||||
################################################################################
|
||||
## Tenant
|
||||
################################################################################
|
||||
|
||||
type Tenant {
|
||||
id: ID!
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
organizationName is the name of the organization.
|
||||
"""
|
||||
organizationName: String
|
||||
"""
|
||||
organizationName is the name of the organization.
|
||||
"""
|
||||
organizationName: String
|
||||
|
||||
"""
|
||||
organizationContactEmail is the email of the organization.
|
||||
"""
|
||||
organizationContactEmail: String
|
||||
"""
|
||||
organizationContactEmail is the email of the organization.
|
||||
"""
|
||||
organizationContactEmail: String
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -35,5 +30,5 @@ type Tenant {
|
||||
################################################################################
|
||||
|
||||
type Query {
|
||||
tenant(id: ID!): Tenant
|
||||
tenant(id: ID!): Tenant
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Db } from "mongodb";
|
||||
import CommonContext from "talk-server/graph/common/context";
|
||||
import { Tenant } from "talk-server/models/tenant";
|
||||
import { User } from "talk-server/models/user";
|
||||
import loaders from "./loaders";
|
||||
@@ -10,14 +11,16 @@ export interface TenantContextOptions {
|
||||
user?: User;
|
||||
}
|
||||
|
||||
export default class TenantContext {
|
||||
export default class TenantContext extends CommonContext {
|
||||
public loaders: ReturnType<typeof loaders>;
|
||||
public mutators: ReturnType<typeof mutators>;
|
||||
public db: Db;
|
||||
public tenant: Tenant;
|
||||
public user?: User;
|
||||
public tenant: Tenant;
|
||||
|
||||
constructor({ user, tenant, db }: TenantContextOptions) {
|
||||
super({ user });
|
||||
|
||||
this.tenant = tenant;
|
||||
this.user = user;
|
||||
this.loaders = loaders(this);
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import DataLoader from "dataloader";
|
||||
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
import { Asset, retrieveManyAssets } from "talk-server/models/asset";
|
||||
import {
|
||||
Asset,
|
||||
FindOrCreateAssetInput,
|
||||
retrieveManyAssets,
|
||||
} from "talk-server/models/asset";
|
||||
import { findOrCreate } from "talk-server/services/assets";
|
||||
|
||||
export default (ctx: TenantContext) => ({
|
||||
findOrCreate: (input: FindOrCreateAssetInput) =>
|
||||
findOrCreate(ctx.db, ctx.tenant, input),
|
||||
asset: new DataLoader<string, Asset | null>(ids =>
|
||||
retrieveManyAssets(ctx.db, ctx.tenant.id, ids)
|
||||
),
|
||||
|
||||
@@ -1,18 +1,48 @@
|
||||
import DataLoader from "dataloader";
|
||||
|
||||
import Context from "talk-server/graph/tenant/context";
|
||||
import {
|
||||
ConnectionInput,
|
||||
retrieveAssetConnection,
|
||||
retrieveMany,
|
||||
retrieveRepliesConnection,
|
||||
AssetToCommentsArgs,
|
||||
CommentToRepliesArgs,
|
||||
GQLCOMMENT_SORT,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import {
|
||||
retrieveCommentAssetConnection,
|
||||
retrieveCommentRepliesConnection,
|
||||
retrieveManyComments,
|
||||
} 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: ConnectionInput) =>
|
||||
retrieveAssetConnection(ctx.db, ctx.tenant.id, assetID, input),
|
||||
forParent: (assetID: string, parentID: string, input: ConnectionInput) =>
|
||||
retrieveRepliesConnection(ctx.db, ctx.tenant.id, assetID, parentID, input),
|
||||
forAsset: (
|
||||
assetID: string,
|
||||
// Apply the graph schema defaults at the loader.
|
||||
{
|
||||
first = 10,
|
||||
orderBy = GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
after,
|
||||
}: AssetToCommentsArgs
|
||||
) =>
|
||||
retrieveCommentAssetConnection(ctx.db, ctx.tenant.id, assetID, {
|
||||
first,
|
||||
orderBy,
|
||||
after,
|
||||
}),
|
||||
forParent: (
|
||||
assetID: string,
|
||||
parentID: string,
|
||||
// Apply the graph schema defaults at the loader.
|
||||
{
|
||||
first = 10,
|
||||
orderBy = GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
after,
|
||||
}: CommentToRepliesArgs
|
||||
) =>
|
||||
retrieveCommentRepliesConnection(ctx.db, ctx.tenant.id, assetID, parentID, {
|
||||
first,
|
||||
orderBy,
|
||||
after,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
import { CreateCommentInput } from "talk-server/graph/tenant/resolvers/mutation";
|
||||
import { GQLCreateCommentInput } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { Comment } from "talk-server/models/comment";
|
||||
import { create } from "talk-server/services/comments";
|
||||
|
||||
export default (ctx: TenantContext) => ({
|
||||
create: (input: CreateCommentInput): Promise<Comment> => {
|
||||
create: (input: GQLCreateCommentInput): Promise<Comment> => {
|
||||
// FIXME: remove tenant + user !
|
||||
return create(ctx.db, ctx.tenant!.id, {
|
||||
return create(ctx.db, ctx.tenant, {
|
||||
author_id: ctx.user!.id,
|
||||
asset_id: input.assetID,
|
||||
body: input.body,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import Context from "talk-server/graph/tenant/context";
|
||||
import { GQLAssetTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { Asset } from "talk-server/models/asset";
|
||||
import { ConnectionInput } from "talk-server/models/comment";
|
||||
|
||||
export default {
|
||||
comments: async (asset: Asset, input: ConnectionInput, ctx: Context) =>
|
||||
const Asset: GQLAssetTypeResolver<Asset> = {
|
||||
comments: (asset, input, ctx) =>
|
||||
ctx.loaders.Comments.forAsset(asset.id, input),
|
||||
// TODO: implement this.
|
||||
isClosed: () => false,
|
||||
};
|
||||
|
||||
export default Asset;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { GQLAuthIntegrationsTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { AuthIntegration, AuthIntegrations } from "talk-server/models/tenant";
|
||||
|
||||
const disabled: AuthIntegration = { enabled: false };
|
||||
|
||||
const AuthIntegrations: GQLAuthIntegrationsTypeResolver<AuthIntegrations> = {
|
||||
local: auth => auth.local || disabled,
|
||||
sso: auth => auth.sso || disabled,
|
||||
oidc: auth => auth.oidc || disabled,
|
||||
google: auth => auth.google || disabled,
|
||||
facebook: auth => auth.facebook || disabled,
|
||||
};
|
||||
|
||||
export default AuthIntegrations;
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GQLAuthSettingsTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { Auth } from "talk-server/models/tenant";
|
||||
|
||||
const AuthSettings: GQLAuthSettingsTypeResolver<Auth> = {
|
||||
integrations: auth => auth.integrations,
|
||||
};
|
||||
|
||||
export default AuthSettings;
|
||||
@@ -1,11 +1,12 @@
|
||||
import Context from "talk-server/graph/tenant/context";
|
||||
import { Comment, ConnectionInput } from "talk-server/models/comment";
|
||||
import { GQLCommentTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { Comment } from "talk-server/models/comment";
|
||||
|
||||
export default {
|
||||
createdAt: async (comment: Comment, _: any, ctx: Context) =>
|
||||
comment.created_at,
|
||||
author: async (comment: Comment, _: any, ctx: Context) =>
|
||||
const Comment: GQLCommentTypeResolver<Comment> = {
|
||||
createdAt: comment => comment.created_at,
|
||||
author: (comment, input, ctx) =>
|
||||
ctx.loaders.Users.user.load(comment.author_id),
|
||||
replies: async (comment: Comment, input: ConnectionInput, ctx: Context) =>
|
||||
replies: (comment, input, ctx) =>
|
||||
ctx.loaders.Comments.forParent(comment.asset_id, comment.id, input),
|
||||
};
|
||||
|
||||
export default Comment;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { GQLFacebookAuthIntegrationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { FacebookAuthIntegration } from "talk-server/models/tenant";
|
||||
|
||||
const FacebookAuthIntegration: GQLFacebookAuthIntegrationTypeResolver<
|
||||
FacebookAuthIntegration
|
||||
> = {
|
||||
config: auth => auth,
|
||||
};
|
||||
|
||||
export default FacebookAuthIntegration;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { GQLGoogleAuthIntegrationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { GoogleAuthIntegration } from "talk-server/models/tenant";
|
||||
|
||||
const GoogleAuthIntegration: GQLGoogleAuthIntegrationTypeResolver<
|
||||
GoogleAuthIntegration
|
||||
> = {
|
||||
config: auth => auth,
|
||||
};
|
||||
|
||||
export default GoogleAuthIntegration;
|
||||
@@ -1,13 +1,33 @@
|
||||
import Cursor from "../../common/scalars/cursor";
|
||||
import Asset from "./asset";
|
||||
import Comment from "./comment";
|
||||
import Mutation from "./mutation";
|
||||
import Query from "./query";
|
||||
import Cursor from "talk-server/graph/common/scalars/cursor";
|
||||
import { GQLResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
export default {
|
||||
import Asset from "./asset";
|
||||
import AuthIntegrations from "./auth_integrations";
|
||||
import AuthSettings from "./auth_settings";
|
||||
import Comment from "./comment";
|
||||
import FacebookAuthIntegration from "./facebook_auth_integration";
|
||||
import GoogleAuthIntegration from "./google_auth_integration";
|
||||
import LocalAuthIntegration from "./local_auth_integration";
|
||||
import Mutation from "./mutation";
|
||||
import OIDCAuthIntegration from "./oidc_auth_integration";
|
||||
import Profile from "./profile";
|
||||
import Query from "./query";
|
||||
import SSOAuthIntegration from "./sso_auth_integration";
|
||||
|
||||
const Resolvers: GQLResolver = {
|
||||
Asset,
|
||||
AuthIntegrations,
|
||||
AuthSettings,
|
||||
Comment,
|
||||
FacebookAuthIntegration,
|
||||
GoogleAuthIntegration,
|
||||
LocalAuthIntegration,
|
||||
OIDCAuthIntegration,
|
||||
SSOAuthIntegration,
|
||||
Cursor,
|
||||
Query,
|
||||
Mutation,
|
||||
Profile,
|
||||
Query,
|
||||
};
|
||||
|
||||
export default Resolvers;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GQLLocalAuthIntegrationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { LocalAuthIntegration } from "talk-server/models/tenant";
|
||||
|
||||
const LocalAuthIntegration: GQLLocalAuthIntegrationTypeResolver<
|
||||
LocalAuthIntegration
|
||||
> = {};
|
||||
|
||||
export default LocalAuthIntegration;
|
||||
@@ -1,23 +1,7 @@
|
||||
import { ClientMutationProps } from "talk-server/graph/common/resolvers/mutation";
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
import { Comment } from "talk-server/models/comment";
|
||||
import { GQLMutationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
export interface CreateCommentInput extends ClientMutationProps {
|
||||
assetID: string;
|
||||
parentID?: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface CreateCommentPayload extends ClientMutationProps {
|
||||
comment: Comment;
|
||||
}
|
||||
|
||||
const Mutation = {
|
||||
createComment: async (
|
||||
source: void,
|
||||
input: CreateCommentInput,
|
||||
ctx: TenantContext
|
||||
): Promise<CreateCommentPayload> => ({
|
||||
const Mutation: GQLMutationTypeResolver<void> = {
|
||||
createComment: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Comment.create(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { GQLOIDCAuthIntegrationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { OIDCAuthIntegration } from "talk-server/models/tenant";
|
||||
|
||||
const OIDCAuthIntegration: GQLOIDCAuthIntegrationTypeResolver<
|
||||
OIDCAuthIntegration
|
||||
> = {
|
||||
config: auth => auth,
|
||||
};
|
||||
|
||||
export default OIDCAuthIntegration;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { GQLProfileTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { Profile } from "talk-server/models/user";
|
||||
|
||||
const resolveType: GQLProfileTypeResolver<Profile> = profile => {
|
||||
switch (profile.type) {
|
||||
case "local":
|
||||
return "LocalProfile";
|
||||
case "oidc":
|
||||
return "OIDCProfile";
|
||||
case "sso":
|
||||
return "SSOProfile";
|
||||
default:
|
||||
// TODO: replace with better error.
|
||||
throw new Error("invalid profile type");
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
__resolveType: resolveType,
|
||||
};
|
||||
@@ -1,15 +1,11 @@
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
import { GQLQueryTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
export default {
|
||||
asset: async (
|
||||
source: void,
|
||||
{ id }: { id: string; url: string },
|
||||
ctx: TenantContext
|
||||
) => ctx.loaders.Assets.asset.load(id),
|
||||
comment: async (
|
||||
source: void,
|
||||
{ id }: { id: string; url: string },
|
||||
ctx: TenantContext
|
||||
) => (id ? ctx.loaders.Comments.comment.load(id) : null),
|
||||
settings: async (parent: any, args: any, ctx: TenantContext) => ctx.tenant,
|
||||
const Query: GQLQueryTypeResolver<void> = {
|
||||
asset: (source, args, ctx) => ctx.loaders.Assets.findOrCreate(args),
|
||||
comment: (source, { id }, ctx) =>
|
||||
id ? ctx.loaders.Comments.comment.load(id) : null,
|
||||
settings: (source, args, ctx) => ctx.tenant,
|
||||
me: (source, args, ctx) => ctx.user,
|
||||
};
|
||||
|
||||
export default Query;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { GQLSSOAuthIntegrationTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { SSOAuthIntegration } from "talk-server/models/tenant";
|
||||
|
||||
const SSOAuthIntegration: GQLSSOAuthIntegrationTypeResolver<
|
||||
SSOAuthIntegration
|
||||
> = {
|
||||
config: auth => auth,
|
||||
};
|
||||
|
||||
export default SSOAuthIntegration;
|
||||
@@ -1,6 +1,14 @@
|
||||
import { attachDirectiveResolvers, IResolvers } from "graphql-tools";
|
||||
|
||||
import { loadSchema } from "talk-common/graphql";
|
||||
import auth from "talk-server/graph/common/directives/auth";
|
||||
import resolvers from "talk-server/graph/tenant/resolvers";
|
||||
|
||||
export default function getTenantSchema() {
|
||||
return loadSchema("tenant", resolvers);
|
||||
const schema = loadSchema("tenant", resolvers as IResolvers);
|
||||
|
||||
// Attach the directive resolvers.
|
||||
attachDirectiveResolvers(schema, { auth });
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
################################################################################
|
||||
## Custom Directives
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
auth is a directive that will enforce authorization rules on the schema
|
||||
definition. It will restrict the viewer of the field based on roles or if the
|
||||
`userIDField` is specified, it will see if the current users ID equals the field
|
||||
specified. This allows users that own a specific resource (like a comment, or a
|
||||
flag) see their own content, but restrict it to everyone else.
|
||||
"""
|
||||
directive @auth(roles: [USER_ROLE!], userIDField: String) on FIELD_DEFINITION
|
||||
|
||||
################################################################################
|
||||
## Custom Scalar Types
|
||||
################################################################################
|
||||
@@ -31,9 +44,9 @@ enum MODERATION_MODE {
|
||||
}
|
||||
|
||||
"""
|
||||
Wordlist describes all the available wordlists.
|
||||
WordlistSettings describes all the available wordlists.
|
||||
"""
|
||||
type Wordlist {
|
||||
type WordlistSettings {
|
||||
"""
|
||||
banned words will by default reject the comment if it is found.
|
||||
"""
|
||||
@@ -45,12 +58,129 @@ type Wordlist {
|
||||
suspect: [String!]!
|
||||
}
|
||||
|
||||
# Settings stores the global settings for a given installation.
|
||||
################################################################################
|
||||
## AuthSettings
|
||||
################################################################################
|
||||
|
||||
##########################
|
||||
## LocalAuthIntegration
|
||||
##########################
|
||||
|
||||
type LocalAuthIntegration {
|
||||
enabled: Boolean!
|
||||
}
|
||||
|
||||
##########################
|
||||
## SSOAuthIntegration
|
||||
##########################
|
||||
|
||||
type SSOAuthIntegrationConfig {
|
||||
key: String!
|
||||
|
||||
"""
|
||||
displayNameEnable when enabled, will allow Users to set and view their
|
||||
displayName's.
|
||||
"""
|
||||
displayNameEnable: Boolean!
|
||||
}
|
||||
|
||||
type SSOAuthIntegration {
|
||||
enabled: Boolean!
|
||||
config: SSOAuthIntegrationConfig @auth(roles: [ADMIN])
|
||||
}
|
||||
|
||||
##########################
|
||||
## OIDCAuthIntegration
|
||||
##########################
|
||||
|
||||
type OIDCAuthIntegrationConfig {
|
||||
clientID: String!
|
||||
clientSecret: String!
|
||||
authorizationURL: String!
|
||||
tokenURL: String!
|
||||
|
||||
"""
|
||||
displayNameEnable when enabled, will allow Users to set and view their
|
||||
displayName's.
|
||||
"""
|
||||
displayNameEnable: Boolean!
|
||||
}
|
||||
|
||||
type OIDCAuthIntegrationOptions {
|
||||
name: String!
|
||||
}
|
||||
|
||||
type OIDCAuthIntegration {
|
||||
enabled: Boolean!
|
||||
options: OIDCAuthIntegrationOptions
|
||||
config: SSOAuthIntegrationConfig @auth(roles: [ADMIN])
|
||||
}
|
||||
|
||||
##########################
|
||||
## GoogleAuthIntegration
|
||||
##########################
|
||||
|
||||
type GoogleAuthIntegrationConfig {
|
||||
clientID: String!
|
||||
clientSecret: String!
|
||||
}
|
||||
|
||||
type GoogleAuthIntegration {
|
||||
enabled: Boolean!
|
||||
config: GoogleAuthIntegrationConfig @auth(roles: [ADMIN])
|
||||
}
|
||||
|
||||
##########################
|
||||
## FacebookAuthIntegration
|
||||
##########################
|
||||
|
||||
type FacebookAuthIntegrationConfig {
|
||||
clientID: String!
|
||||
clientSecret: String!
|
||||
}
|
||||
|
||||
type FacebookAuthIntegration {
|
||||
enabled: Boolean!
|
||||
config: FacebookAuthIntegrationConfig @auth(roles: [ADMIN])
|
||||
}
|
||||
|
||||
type AuthIntegrations {
|
||||
local: LocalAuthIntegration!
|
||||
sso: SSOAuthIntegration!
|
||||
oidc: OIDCAuthIntegration!
|
||||
google: GoogleAuthIntegration!
|
||||
facebook: FacebookAuthIntegration!
|
||||
}
|
||||
|
||||
"""
|
||||
AuthSettings contains all the settings related to authentication and
|
||||
authorization.
|
||||
"""
|
||||
type AuthSettings {
|
||||
"""
|
||||
integrations are the set of configurations for the variations of
|
||||
authentication solutions.
|
||||
"""
|
||||
integrations: AuthIntegrations!
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Settings
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
Settings stores the global settings for a given Tenant.
|
||||
"""
|
||||
type Settings {
|
||||
"""
|
||||
domain is the domain that is associated with this Tenant.
|
||||
"""
|
||||
domain: String @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
moderation is the moderation mode for all Asset's on the site.
|
||||
"""
|
||||
moderation: MODERATION_MODE!
|
||||
moderation: MODERATION_MODE @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
Enables a requirement for email confirmation before a user can login.
|
||||
@@ -87,13 +217,13 @@ type Settings {
|
||||
"""
|
||||
premodLinksEnable will put all comments that contain links into premod.
|
||||
"""
|
||||
premodLinksEnable: Boolean!
|
||||
premodLinksEnable: Boolean @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
autoCloseStream when true will auto close the stream when the `closeTimeout`
|
||||
amount of seconds have been reached.
|
||||
"""
|
||||
autoCloseStream: Boolean!
|
||||
autoCloseStream: Boolean! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
customCssUrl is the URL of the custom CSS used to display on the frontend.
|
||||
@@ -152,18 +282,79 @@ type Settings {
|
||||
"""
|
||||
wordlist will return a given list of words.
|
||||
"""
|
||||
wordlist: Wordlist!
|
||||
wordlist: WordlistSettings @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
domains will return a given list of whitelisted domains.
|
||||
"""
|
||||
domains: [String!]!
|
||||
domains: [String!] @auth(roles: [ADMIN]) @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
auth contains all the settings related to authentication and authorization.
|
||||
"""
|
||||
auth: AuthSettings!
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## User
|
||||
################################################################################
|
||||
|
||||
enum USER_ROLE {
|
||||
COMMENTER
|
||||
MODERATOR
|
||||
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
|
||||
}
|
||||
|
||||
type LocalProfile {
|
||||
id: String!
|
||||
}
|
||||
|
||||
type OIDCProfile {
|
||||
id: String!
|
||||
provider: String!
|
||||
}
|
||||
|
||||
type SSOProfile {
|
||||
id: String!
|
||||
}
|
||||
|
||||
union Profile = LocalProfile | OIDCProfile | SSOProfile
|
||||
|
||||
"""
|
||||
User is someone that leaves Comments, and logs in.
|
||||
"""
|
||||
@@ -176,7 +367,22 @@ type User {
|
||||
"""
|
||||
username is the name of the User visible to other Users.
|
||||
"""
|
||||
username: String!
|
||||
username: String
|
||||
|
||||
"""
|
||||
displayName is provided optionally when enabled and available.
|
||||
"""
|
||||
displayName: String
|
||||
|
||||
"""
|
||||
profiles is the array of profiles assigned to the user.
|
||||
"""
|
||||
profiles: [Profile!] @auth(roles: [ADMIN, MODERATOR], userIDField: "id")
|
||||
|
||||
"""
|
||||
role is the current role of the User.
|
||||
"""
|
||||
role: USER_ROLE! @auth(roles: [ADMIN, MODERATOR], userIDField: "id")
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -391,9 +597,9 @@ type Query {
|
||||
assets(cursor: Cursor, limit: Int = 10): AssetsConnection
|
||||
|
||||
"""
|
||||
asset is the Asset specified by its ID.
|
||||
asset is the Asset specified by its ID/URL.
|
||||
"""
|
||||
asset(id: ID!): Asset
|
||||
asset(id: ID, url: String): Asset
|
||||
|
||||
"""
|
||||
me is the current logged in User.
|
||||
@@ -463,7 +669,7 @@ type Mutation {
|
||||
"""
|
||||
createComment will create a Comment as the current logged in User.
|
||||
"""
|
||||
createComment(input: CreateCommentInput!): CreateCommentPayload
|
||||
createComment(input: CreateCommentInput!): CreateCommentPayload @auth
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
||||
Reference in New Issue
Block a user