mirror of
https://github.com/wassname/talk.git
synced 2026-07-12 21:56:59 +08:00
Merge branch 'next' into next-respect
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
export const adminHandler: RequestHandler = (req, res) => {
|
||||
res.render("admin");
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import { RequestHandler } from "express";
|
||||
|
||||
export const streamHandler: RequestHandler = (req, res) => {
|
||||
res.render("stream");
|
||||
};
|
||||
@@ -9,7 +9,7 @@ export const nocacheMiddleware: RequestHandler = (req, res, next) => {
|
||||
next();
|
||||
};
|
||||
|
||||
export const cacheHeadersMiddleware = (duration: string): RequestHandler => {
|
||||
export const cacheHeadersMiddleware = (duration?: string): RequestHandler => {
|
||||
const maxAge = duration ? Math.floor(ms(duration) / 1000) : false;
|
||||
if (!maxAge) {
|
||||
return nocacheMiddleware;
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
import express from "express";
|
||||
import passport from "passport";
|
||||
|
||||
import {
|
||||
logoutHandler,
|
||||
signupHandler,
|
||||
} from "talk-server/app/handlers/auth/local";
|
||||
import { streamHandler } from "talk-server/app/handlers/embed/stream";
|
||||
import { apiErrorHandler } from "talk-server/app/middleware/error";
|
||||
import { errorLogger } from "talk-server/app/middleware/logging";
|
||||
import { wrapAuthn } from "talk-server/app/middleware/passport";
|
||||
import tenantMiddleware from "talk-server/app/middleware/tenant";
|
||||
import managementGraphMiddleware from "talk-server/graph/management/middleware";
|
||||
import tenantGraphMiddleware from "talk-server/graph/tenant/middleware";
|
||||
|
||||
import {
|
||||
cacheHeadersMiddleware,
|
||||
nocacheMiddleware,
|
||||
} from "talk-server/app/middleware/cacheHeaders";
|
||||
import { AppOptions } from "./index";
|
||||
import playground from "./middleware/playground";
|
||||
|
||||
async function createManagementRouter(app: AppOptions, options: RouterOptions) {
|
||||
const router = express.Router();
|
||||
|
||||
// Management API
|
||||
router.use(
|
||||
"/graphql",
|
||||
express.json(),
|
||||
await managementGraphMiddleware(
|
||||
app.schemas.management,
|
||||
app.config,
|
||||
app.mongo
|
||||
)
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
async function createTenantRouter(app: AppOptions, options: RouterOptions) {
|
||||
const router = express.Router();
|
||||
|
||||
// Tenant identification middleware.
|
||||
router.use(tenantMiddleware({ cache: app.tenantCache }));
|
||||
|
||||
// Setup Passport middleware.
|
||||
router.use(options.passport.initialize());
|
||||
|
||||
// Setup auth routes.
|
||||
router.use("/auth", createNewAuthRouter(app, options));
|
||||
|
||||
// Tenant API
|
||||
router.use(
|
||||
"/graphql",
|
||||
express.json(),
|
||||
// Any users may submit their GraphQL requests with authentication, this
|
||||
// middleware will unpack their user into the request.
|
||||
options.passport.authenticate("jwt", { session: false }),
|
||||
await tenantGraphMiddleware({
|
||||
schema: app.schemas.tenant,
|
||||
config: app.config,
|
||||
mongo: app.mongo,
|
||||
redis: app.redis,
|
||||
queue: app.queue,
|
||||
})
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
function createNewAuthRouter(app: AppOptions, options: RouterOptions) {
|
||||
const router = express.Router();
|
||||
|
||||
// Mount the passport routes.
|
||||
router.delete(
|
||||
"/",
|
||||
options.passport.authenticate("jwt", { session: false }),
|
||||
logoutHandler({ redis: app.redis })
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/local",
|
||||
express.json(),
|
||||
wrapAuthn(options.passport, app.signingConfig, "local")
|
||||
);
|
||||
router.post(
|
||||
"/local/signup",
|
||||
express.json(),
|
||||
signupHandler({ db: app.mongo, signingConfig: app.signingConfig })
|
||||
);
|
||||
|
||||
router.get("/oidc", wrapAuthn(options.passport, app.signingConfig, "oidc"));
|
||||
router.get(
|
||||
"/oidc/callback",
|
||||
wrapAuthn(options.passport, app.signingConfig, "oidc")
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
async function createAPIRouter(app: AppOptions, options: RouterOptions) {
|
||||
// Create a router.
|
||||
const router = express.Router();
|
||||
|
||||
// Configure the tenant routes.
|
||||
router.use("/tenant", await createTenantRouter(app, options));
|
||||
|
||||
// Configure the management routes.
|
||||
router.use("/management", await createManagementRouter(app, options));
|
||||
|
||||
// General API error handler.
|
||||
router.use(errorLogger);
|
||||
router.use(apiErrorHandler);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
export interface RouterOptions {
|
||||
/**
|
||||
* passport is the instance of the Authenticator that can be used to create
|
||||
* and mount new authentication middleware.
|
||||
*/
|
||||
passport: passport.Authenticator;
|
||||
}
|
||||
|
||||
export async function createRouter(app: AppOptions, options: RouterOptions) {
|
||||
// Create a router.
|
||||
const router = express.Router();
|
||||
|
||||
router.use("/api", nocacheMiddleware, await createAPIRouter(app, options));
|
||||
|
||||
if (app.config.get("env") === "development") {
|
||||
// Tenant GraphiQL
|
||||
router.get(
|
||||
"/tenant/graphiql",
|
||||
playground({
|
||||
endpoint: "/api/tenant/graphql",
|
||||
subscriptionEndpoint: "/api/tenant/live",
|
||||
})
|
||||
);
|
||||
|
||||
// Management GraphiQL
|
||||
router.get(
|
||||
"/management/graphiql",
|
||||
playground({
|
||||
endpoint: "/api/management/graphql",
|
||||
subscriptionEndpoint: "/api/management/live",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Handle the stream handler.
|
||||
router.get("/embed/stream", cacheHeadersMiddleware("1h"), streamHandler);
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import express from "express";
|
||||
|
||||
import { AppOptions } from "talk-server/app";
|
||||
import {
|
||||
logoutHandler,
|
||||
signupHandler,
|
||||
} from "talk-server/app/handlers/auth/local";
|
||||
import { wrapAuthn } from "talk-server/app/middleware/passport";
|
||||
import { RouterOptions } from "talk-server/app/router/types";
|
||||
|
||||
export function createNewAuthRouter(app: AppOptions, options: RouterOptions) {
|
||||
const router = express.Router();
|
||||
|
||||
// Mount the passport routes.
|
||||
router.delete(
|
||||
"/",
|
||||
options.passport.authenticate("jwt", { session: false }),
|
||||
logoutHandler({ redis: app.redis })
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/local",
|
||||
express.json(),
|
||||
wrapAuthn(options.passport, app.signingConfig, "local")
|
||||
);
|
||||
router.post(
|
||||
"/local/signup",
|
||||
express.json(),
|
||||
signupHandler({ db: app.mongo, signingConfig: app.signingConfig })
|
||||
);
|
||||
|
||||
router.get("/oidc", wrapAuthn(options.passport, app.signingConfig, "oidc"));
|
||||
router.get(
|
||||
"/oidc/callback",
|
||||
wrapAuthn(options.passport, app.signingConfig, "oidc")
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import express from "express";
|
||||
import passport from "passport";
|
||||
|
||||
import { AppOptions } from "talk-server/app";
|
||||
import { apiErrorHandler } from "talk-server/app/middleware/error";
|
||||
import { errorLogger } from "talk-server/app/middleware/logging";
|
||||
|
||||
import { createManagementRouter } from "./management";
|
||||
import { createTenantRouter } from "./tenant";
|
||||
|
||||
export interface RouterOptions {
|
||||
/**
|
||||
* passport is the instance of the Authenticator that can be used to create
|
||||
* and mount new authentication middleware.
|
||||
*/
|
||||
passport: passport.Authenticator;
|
||||
}
|
||||
|
||||
export async function createAPIRouter(app: AppOptions, options: RouterOptions) {
|
||||
// Create a router.
|
||||
const router = express.Router();
|
||||
|
||||
// Configure the tenant routes.
|
||||
router.use("/tenant", await createTenantRouter(app, options));
|
||||
|
||||
// Configure the management routes.
|
||||
router.use("/management", await createManagementRouter(app));
|
||||
|
||||
// General API error handler.
|
||||
router.use(errorLogger);
|
||||
router.use(apiErrorHandler);
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import express from "express";
|
||||
|
||||
import { AppOptions } from "talk-server/app";
|
||||
import managementGraphMiddleware from "talk-server/graph/management/middleware";
|
||||
|
||||
export async function createManagementRouter(app: AppOptions) {
|
||||
const router = express.Router();
|
||||
|
||||
// Management API
|
||||
router.use(
|
||||
"/graphql",
|
||||
express.json(),
|
||||
await managementGraphMiddleware(
|
||||
app.schemas.management,
|
||||
app.config,
|
||||
app.mongo
|
||||
)
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import express from "express";
|
||||
|
||||
import { AppOptions } from "talk-server/app";
|
||||
import tenantMiddleware from "talk-server/app/middleware/tenant";
|
||||
import { RouterOptions } from "talk-server/app/router/types";
|
||||
import tenantGraphMiddleware from "talk-server/graph/tenant/middleware";
|
||||
|
||||
import { createNewAuthRouter } from "./auth";
|
||||
|
||||
export async function createTenantRouter(
|
||||
app: AppOptions,
|
||||
options: RouterOptions
|
||||
) {
|
||||
const router = express.Router();
|
||||
|
||||
// Tenant identification middleware.
|
||||
router.use(tenantMiddleware({ cache: app.tenantCache }));
|
||||
|
||||
// Setup Passport middleware.
|
||||
router.use(options.passport.initialize());
|
||||
|
||||
// Setup auth routes.
|
||||
router.use("/auth", createNewAuthRouter(app, options));
|
||||
|
||||
// Tenant API
|
||||
router.use(
|
||||
"/graphql",
|
||||
express.json(),
|
||||
// Any users may submit their GraphQL requests with authentication, this
|
||||
// middleware will unpack their user into the request.
|
||||
options.passport.authenticate("jwt", { session: false }),
|
||||
await tenantGraphMiddleware({
|
||||
schema: app.schemas.tenant,
|
||||
config: app.config,
|
||||
mongo: app.mongo,
|
||||
redis: app.redis,
|
||||
queue: app.queue,
|
||||
})
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import express from "express";
|
||||
|
||||
import { cacheHeadersMiddleware } from "talk-server/app/middleware/cacheHeaders";
|
||||
|
||||
export interface ClientTargetHandlerOptions {
|
||||
/**
|
||||
* view is the name of the template to render.
|
||||
*/
|
||||
view: string;
|
||||
|
||||
/**
|
||||
* cacheDuration is the cache duration that a given request should be cached for.
|
||||
*/
|
||||
cacheDuration?: string;
|
||||
}
|
||||
|
||||
export function createClientTargetRouter({
|
||||
view,
|
||||
cacheDuration = "1h",
|
||||
}: ClientTargetHandlerOptions) {
|
||||
// Create a router.
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", cacheHeadersMiddleware(cacheDuration), (req, res) =>
|
||||
res.render(view)
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import express, { Router } from "express";
|
||||
|
||||
import { AppOptions } from "talk-server/app";
|
||||
import { nocacheMiddleware } from "talk-server/app/middleware/cacheHeaders";
|
||||
import playground from "talk-server/app/middleware/playground";
|
||||
import { RouterOptions } from "talk-server/app/router/types";
|
||||
import logger from "talk-server/logger";
|
||||
|
||||
import { createAPIRouter } from "./api";
|
||||
import { createClientTargetRouter } from "./client";
|
||||
|
||||
export async function createRouter(app: AppOptions, options: RouterOptions) {
|
||||
// Create a router.
|
||||
const router = express.Router();
|
||||
|
||||
router.use("/api", nocacheMiddleware, await createAPIRouter(app, options));
|
||||
|
||||
// Attach the GraphiQL if enabled.
|
||||
if (app.config.get("enable_graphiql")) {
|
||||
attachGraphiQL(router, app);
|
||||
}
|
||||
|
||||
// Add the client targets.
|
||||
router.get("/embed/stream", createClientTargetRouter({ view: "stream" }));
|
||||
router.get("/admin", createClientTargetRouter({ view: "admin" }));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* attachGraphiQL will attach the GraphiQL routes to the router.
|
||||
*
|
||||
* @param router the router to attach the GraphiQL routes to
|
||||
* @param app the application to read the configuration from
|
||||
*/
|
||||
function attachGraphiQL(router: Router, app: AppOptions) {
|
||||
if (app.config.get("env") === "production") {
|
||||
logger.warn(
|
||||
"enable_graphiql is enabled, but we're in production mode, this is not recommended"
|
||||
);
|
||||
}
|
||||
|
||||
// Tenant GraphiQL
|
||||
router.get(
|
||||
"/tenant/graphiql",
|
||||
playground({
|
||||
endpoint: "/api/tenant/graphql",
|
||||
subscriptionEndpoint: "/api/tenant/live",
|
||||
})
|
||||
);
|
||||
|
||||
// Management GraphiQL
|
||||
router.get(
|
||||
"/management/graphiql",
|
||||
playground({
|
||||
endpoint: "/api/management/graphql",
|
||||
subscriptionEndpoint: "/api/management/live",
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import passport from "passport";
|
||||
|
||||
export interface RouterOptions {
|
||||
/**
|
||||
* passport is the instance of the Authenticator that can be used to create
|
||||
* and mount new authentication middleware.
|
||||
*/
|
||||
passport: passport.Authenticator;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
retrieveCommentAssetConnection,
|
||||
retrieveCommentParentsConnection,
|
||||
retrieveCommentRepliesConnection,
|
||||
retrieveCommentUserConnection,
|
||||
retrieveManyComments,
|
||||
} from "talk-server/models/comment";
|
||||
import { Connection } from "talk-server/models/connection";
|
||||
@@ -53,6 +54,20 @@ export default (ctx: Context) => ({
|
||||
itemIDs
|
||||
)
|
||||
),
|
||||
forUser: (
|
||||
userID: string,
|
||||
// Apply the graph schema defaults at the loader.
|
||||
{
|
||||
first = 10,
|
||||
orderBy = GQLCOMMENT_SORT.CREATED_AT_DESC,
|
||||
after,
|
||||
}: AssetToCommentsArgs
|
||||
) =>
|
||||
retrieveCommentUserConnection(ctx.mongo, ctx.tenant.id, userID, {
|
||||
first,
|
||||
orderBy,
|
||||
after,
|
||||
}),
|
||||
forAsset: (
|
||||
assetID: string,
|
||||
// Apply the graph schema defaults at the loader.
|
||||
|
||||
@@ -84,6 +84,8 @@ const Comment: GQLCommentTypeResolver<Comment> = {
|
||||
comment.parent_id
|
||||
? ctx.loaders.Comments.parents(comment, input)
|
||||
: createConnection(),
|
||||
asset: (comment, input, ctx) =>
|
||||
ctx.loaders.Assets.asset.load(comment.asset_id),
|
||||
};
|
||||
|
||||
export default Comment;
|
||||
|
||||
@@ -8,6 +8,7 @@ import CommentCounts from "./comment_counts";
|
||||
import Mutation from "./mutation";
|
||||
import Profile from "./profile";
|
||||
import Query from "./query";
|
||||
import User from "./user";
|
||||
|
||||
const Resolvers: GQLResolver = {
|
||||
Asset,
|
||||
@@ -18,6 +19,7 @@ const Resolvers: GQLResolver = {
|
||||
Mutation,
|
||||
Profile,
|
||||
Query,
|
||||
User,
|
||||
};
|
||||
|
||||
export default Resolvers;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GQLUserTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { User } from "talk-server/models/user";
|
||||
|
||||
const User: GQLUserTypeResolver<User> = {
|
||||
comments: (user, input, ctx) => ctx.loaders.Comments.forUser(user.id, input),
|
||||
};
|
||||
|
||||
export default User;
|
||||
@@ -5,7 +5,7 @@
|
||||
"""
|
||||
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
|
||||
`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. If the directive
|
||||
is used without options, it simply requires a logged in user.
|
||||
@@ -761,6 +761,15 @@ type User {
|
||||
role is the current role of the User.
|
||||
"""
|
||||
role: USER_ROLE! @auth(roles: [ADMIN, MODERATOR], userIDField: "id")
|
||||
|
||||
"""
|
||||
comments are the comments of the User.
|
||||
"""
|
||||
comments(
|
||||
first: Int = 10
|
||||
orderBy: COMMENT_SORT = CREATED_AT_DESC
|
||||
after: Cursor
|
||||
): CommentsConnection! @auth(roles: [ADMIN, MODERATOR], userIDField: "id")
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -897,6 +906,11 @@ type Comment {
|
||||
left by the current User on this Comment.
|
||||
"""
|
||||
myActionPresence: ActionPresence
|
||||
|
||||
"""
|
||||
asset is the Asset that the Comment was written on.
|
||||
"""
|
||||
asset: Asset!
|
||||
}
|
||||
|
||||
type PageInfo {
|
||||
|
||||
@@ -422,6 +422,30 @@ export async function retrieveCommentAssetConnection(
|
||||
return retrieveConnection(input, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieveCommentUserConnection returns a Connection<Comment> for a given User's
|
||||
* comments.
|
||||
*
|
||||
* @param db database connection
|
||||
* @param userID the User id for the comment to retrieve
|
||||
* @param input connection configuration
|
||||
*/
|
||||
export async function retrieveCommentUserConnection(
|
||||
db: Db,
|
||||
tenantID: string,
|
||||
userID: string,
|
||||
input: ConnectionInput
|
||||
) {
|
||||
// Create the query.
|
||||
const query = new Query(collection(db)).where({
|
||||
tenant_id: tenantID,
|
||||
author_id: userID,
|
||||
});
|
||||
|
||||
// Return a connection for the comments query.
|
||||
return retrieveConnection(input, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieveConnection returns a Connection<Comment> for the given input and
|
||||
* Query.
|
||||
|
||||
Reference in New Issue
Block a user