diff --git a/source/server/README.md b/source/server/README.md index 3f234329..69197085 100644 --- a/source/server/README.md +++ b/source/server/README.md @@ -8,7 +8,7 @@ As long as no user exist, the application launches in "open" mode. An unauthenticated request can be used to create the first user : ``` -curl -XPOST -H "Content-Type: application/json" -d '{"username":"<...>", "password":"<...>", "email":"<...>", "isAdministrator": true}' "http://:/api/v1/users" +curl -XPOST -H "Content-Type: application/json" -d '{"username":"<...>", "password":"<...>", "email":"<...>", "isAdministrator": true}' "http://:/users" ``` Then restart the application to enable permissions management diff --git a/source/server/__test_fixtures/fixtures.ts b/source/server/__test_fixtures/fixtures.ts new file mode 100644 index 00000000..5972268b --- /dev/null +++ b/source/server/__test_fixtures/fixtures.ts @@ -0,0 +1,5 @@ +import path from "path"; +import { fileURLToPath } from 'url'; + + +export const fixturesDir = path.dirname(fileURLToPath(import.meta.url)); \ No newline at end of file diff --git a/source/server/index.ts b/source/server/index.ts index 7aa85f6d..0462bab5 100644 --- a/source/server/index.ts +++ b/source/server/index.ts @@ -16,7 +16,7 @@ */ import path from "path"; -import createServer from "./server.js"; +import createServer from "./routes/index.js"; import config from "./utils/config.js"; //@ts-ignore diff --git a/source/server/integration.test.ts b/source/server/integration.test.ts index 241ee623..f12c1f83 100644 --- a/source/server/integration.test.ts +++ b/source/server/integration.test.ts @@ -1,16 +1,14 @@ import fs from "fs/promises"; import path from "path"; -import { fileURLToPath } from 'url'; -import {tmpdir} from "os"; import request from "supertest"; -import createServer from "./server.js"; -import Vfs, { DocProps, WriteFileParams } from "./vfs/index.js"; +import Vfs from "./vfs/index.js"; import User from "./auth/User.js"; -import { Element, xml2js } from "xml-js"; import UserManager from "./auth/UserManager.js"; -const thisDir = path.dirname(fileURLToPath(import.meta.url)); +import { fixturesDir } from "./__test_fixtures/fixtures.js"; + + describe("Web Server Integration", function(){ let vfs :Vfs, userManager :UserManager, user :User, admin :User; @@ -56,7 +54,7 @@ describe("Web Server Integration", function(){ await request(this.server).put("/scenes/foo/models/bar.glb").expect(401); }); it("can't fetch user list", async function(){ - await request(this.server).get("/api/v1/users") + await request(this.server).get("/users") .expect(401); }); }); @@ -64,7 +62,7 @@ describe("Web Server Integration", function(){ describe("(author)", function(){ this.beforeEach(async function(){ this.agent = request.agent(this.server); - await this.agent.post("/api/v1/login") + await this.agent.post("/auth/login") .send({username: user.username, password: "12345678"}) .set("Content-Type", "application/json") .set("Accept", "") @@ -72,8 +70,8 @@ describe("Web Server Integration", function(){ }); it("can create a new scene", async function(){ - let content = await fs.readFile(path.join(thisDir, "__test_fixtures/cube.glb")); - let r = await this.agent.post("/api/v1/scenes/bar") + let content = await fs.readFile(path.join(fixturesDir, "cube.glb")); + let r = await this.agent.post("/scenes/bar") .set("Content-Type", "application/octet-stream") .send(content) .expect(201); @@ -86,7 +84,7 @@ describe("Web Server Integration", function(){ }); it("can upload a glb model in an existing scene", async function(){ - let content = await fs.readFile(path.join(thisDir, "__test_fixtures/cube.glb")); + let content = await fs.readFile(path.join(fixturesDir, "cube.glb")); await this.agent.put("/scenes/foo/models/baz.glb") .send(content) .expect(201); @@ -137,11 +135,11 @@ describe("Web Server Integration", function(){ it("can grant permissions", async function(){ let dave = await userManager.addUser("dave", "12345678"); - await this.agent.patch("/api/v1/scenes/foo/permissions") + await this.agent.patch("/auth/access/foo") .send({username: "dave", access: "write"}) .expect(204); - let r = await this.agent.get("/api/v1/scenes/foo/permissions") + let r = await this.agent.get("/auth/access/foo") .expect(200) .expect("Content-Type", "application/json; charset=utf-8"); expect(r, JSON.stringify(r.body)).to.have.property("body").to.deep.equal([ @@ -153,11 +151,11 @@ describe("Web Server Integration", function(){ }); it("can make a model private", async function(){ - await this.agent.patch("/api/v1/scenes/foo/permissions") + await this.agent.patch("/auth/access/foo") .send({username: "default", access: "none"}) .expect(204); - let r = await this.agent.get("/api/v1/scenes/foo/permissions") + let r = await this.agent.get("/auth/access/foo") .expect(200) .expect("Content-Type", "application/json; charset=utf-8"); expect(r).to.have.property("body").to.deep.equal([ @@ -168,11 +166,11 @@ describe("Web Server Integration", function(){ }); it("can remove a user's special permissions", async function(){ - await this.agent.patch("/api/v1/scenes/foo/permissions") + await this.agent.patch("/auth/access/foo") .send({username: user.username, access: null}) .expect(204); - let r = await this.agent.get("/api/v1/scenes/foo/permissions") + let r = await this.agent.get("/auth/access/foo") .expect(200) .expect("Content-Type", "application/json; charset=utf-8"); expect(r).to.have.property("body").to.deep.equal([ @@ -189,7 +187,7 @@ describe("Web Server Integration", function(){ eve = await userManager.addUser("eve", "12345678"); this.agent = request.agent(this.server); - await this.agent.post("/api/v1/login") + await this.agent.post("/auth/login") .send({username: eve.username, password: "12345678"}) .set("Content-Type", "application/json") .set("Accept", "") diff --git a/source/server/routes/admin/index.ts b/source/server/routes/admin/index.ts new file mode 100644 index 00000000..6661e869 --- /dev/null +++ b/source/server/routes/admin/index.ts @@ -0,0 +1,26 @@ + +import { Router } from "express"; + +import { isAdministrator } from "../../utils/locals.js"; +import wrap from "../../utils/wrapAsync.js"; +import handleGetStats from "./stats/index.js"; +import handleMailtest from "./mailtest.js"; + + +const router = Router(); + +/** Configure cache behaviour for the whole API + * Settings can be changed individually further down the line + */ +router.use((req, res, next)=>{ + //Browser should always make the request + res.set("Cache-Control", "no-cache"); + next(); +}); + + + +router.get("/admin/stats", isAdministrator, wrap(handleGetStats)); +router.post("/admin/mailtest", isAdministrator, wrap(handleMailtest)); + +export default router; diff --git a/source/server/routes/api/v1/admin/mailtest.ts b/source/server/routes/admin/mailtest.ts similarity index 79% rename from source/server/routes/api/v1/admin/mailtest.ts rename to source/server/routes/admin/mailtest.ts index 608c0abf..d1ed88a5 100644 --- a/source/server/routes/api/v1/admin/mailtest.ts +++ b/source/server/routes/admin/mailtest.ts @@ -1,7 +1,7 @@ import { Request, Response } from "express"; -import sendmail from "../../../../utils/mails/send.js"; -import { getLocals, getUser } from "../../../../utils/locals.js"; -import { BadRequestError } from "../../../../utils/errors.js"; +import sendmail from "../../utils/mails/send.js"; +import { getLocals, getUser } from "../../utils/locals.js"; +import { BadRequestError } from "../../utils/errors.js"; /** * Send a test email diff --git a/source/server/routes/api/v1/admin/stats/index.ts b/source/server/routes/admin/stats/index.ts similarity index 89% rename from source/server/routes/api/v1/admin/stats/index.ts rename to source/server/routes/admin/stats/index.ts index 94b7a3ab..3e1da7e2 100644 --- a/source/server/routes/api/v1/admin/stats/index.ts +++ b/source/server/routes/admin/stats/index.ts @@ -1,5 +1,5 @@ import { Request, Response } from "express"; -import { getVfs } from "../../../../../utils/locals.js"; +import { getVfs } from "../../../utils/locals.js"; diff --git a/source/server/routes/api/v1/index.ts b/source/server/routes/api/v1/index.ts deleted file mode 100644 index 9af7e346..00000000 --- a/source/server/routes/api/v1/index.ts +++ /dev/null @@ -1,90 +0,0 @@ - -import path from "path"; -import { Router } from "express"; -import { rateLimit } from 'express-rate-limit' - -import User from "../../../auth/User.js"; -import UserManager from "../../../auth/UserManager.js"; -import { BadRequestError } from "../../../utils/errors.js"; -import { canAdmin, canRead, either, getUserManager, isAdministrator, isAdministratorOrOpen, isUser } from "../../../utils/locals.js"; -import wrap from "../../../utils/wrapAsync.js"; -import bodyParser from "body-parser"; -import { getLogin, getLoginLink, sendLoginLink, postLogin } from "./login.js"; -import { postLogout } from "./logout.js"; -import postScene from "./scenes/scene/post.js"; -import getScenes from "./scenes/get.js"; -import getSceneHistory from "./scenes/scene/history/get.js"; -import getFiles from "./scenes/scene/files/get.js"; - -import getScene from "./scenes/scene/get.js"; -import getPermissions from "./scenes/scene/permissions/get.js"; -import patchPermissions from "./scenes/scene/permissions/patch.js"; -import postUser from "./users/post.js"; -import handleDeleteUser from "./users/uid/delete.js"; -import { handlePatchUser } from "./users/uid/patch.js"; -import { postSceneHistory } from "./scenes/scene/history/post.js"; -import handleGetStats from "./admin/stats/index.js"; -import postScenes from "./scenes/post.js"; -import patchScene from "./scenes/scene/patch.js"; -import handleMailtest from "./admin/mailtest.js"; - - -const router = Router(); - -/** Configure cache behaviour for the whole API - * Settings can be changed individually further down the line - */ -router.use((req, res, next)=>{ - //Browser should always make the request - res.set("Cache-Control", "no-cache"); - next(); -}); - -router.get("/admin/stats", isAdministrator, wrap(handleGetStats)); -router.post("/admin/mailtest", isAdministrator, wrap(handleMailtest)); - -router.use("/login", (req, res, next)=>{ - res.append("Cache-Control", "private"); - next(); -}); -router.get("/login", wrap(getLogin)); -router.post("/login", bodyParser.json(), postLogin); -router.get("/login/:username/link", isAdministrator, wrap(getLoginLink)); -router.post("/login/:username/link", either(isAdministrator, rateLimit({ - //Special case of real low rate-limiting for non-admin users to send emails - windowMs: 1 * 60 * 1000, // 1 minute - limit: 1, // Limit each IP to 1 request per `window`. - standardHeaders: 'draft-7', - legacyHeaders: false, -})), wrap(sendLoginLink)); - -router.post("/logout", postLogout); - - -router.get("/users", isAdministrator, wrap(async (req, res)=>{ - let userManager :UserManager = getUserManager(req); - //istanbul ignore if - if(!userManager) throw new Error("Badly configured app : userManager is not defined in app.locals"); - let users = await userManager.getUsers(true); - res.status(200).send(users); -})); - -router.post("/users", isAdministratorOrOpen, bodyParser.json(), wrap(postUser)); -router.delete("/users/:uid", isAdministrator, wrap(handleDeleteUser)); -router.patch("/users/:uid", bodyParser.json(), wrap(handlePatchUser)); - -router.get("/scenes", wrap(getScenes)); -router.post("/scenes", isAdministrator, wrap(postScenes)); - -router.post("/scenes/:scene", isUser, wrap(postScene)); -router.patch("/scenes/:scene", canAdmin, bodyParser.json(), wrap(patchScene)); - -router.use("/scenes/:scene", canRead); -router.get("/scenes/:scene/history", wrap(getSceneHistory)); -router.post("/scenes/:scene/history", canAdmin, bodyParser.json(), wrap(postSceneHistory)); -router.get("/scenes/:scene", wrap(getScene)); -router.get("/scenes/:scene/files", wrap(getFiles)); -router.get("/scenes/:scene/permissions", wrap(getPermissions)); -router.patch("/scenes/:scene/permissions", canAdmin, bodyParser.json(), wrap(patchPermissions)); - -export default router; diff --git a/source/server/routes/api/v1/scenes/scene/files/get.ts b/source/server/routes/api/v1/scenes/scene/files/get.ts deleted file mode 100644 index 725909ea..00000000 --- a/source/server/routes/api/v1/scenes/scene/files/get.ts +++ /dev/null @@ -1,19 +0,0 @@ - -import { Request, Response } from "express"; -import toCsv from "../../../../../../utils/csv.js"; -import { getVfs } from "../../../../../../utils/locals.js"; - - - -export default async function getFiles(req :Request, res :Response){ - let vfs = getVfs(req); - let {scene} = req.params; - let {id} = await vfs.getScene(scene); - let files = await vfs.listFiles(id); - - res.format({ - "application/json":()=>res.status(200).send(files), - "text/csv": ()=> res.status(200).send(toCsv(files)), - "text/plain": ()=> res.status(200).send(files.map(f=>`${f.name}: ${f.hash || "REMOVED"} <${f.author}>`).join("\n")+"\n"), - }); -}; diff --git a/source/server/routes/apipaths.yml b/source/server/routes/apipaths.yml index 7837a81c..38eaf840 100644 --- a/source/server/routes/apipaths.yml +++ b/source/server/routes/apipaths.yml @@ -5,64 +5,8 @@ info: servers: - url: https://ecorpus.holusion.com paths: - /admin/stats: - get: - description: get server stats - /admin/mailtest: - post: - description: sends a test email - /login: - get: - description: get login data - post: - description: log-in to the server - /login/{username}/link: - get: - description: get a login link for this user - post: - description: generate and send a login link for this user - /logout: - post: - description: delete this request's credentials - /users: - get: - description: get a list of registered users - post: - description: create a new user - /users/{uid}: - delete: - description: delete a user - patch: - description: change a user's data - - /scenes: - get: - description: get a list of scenes with optional search parameters - post: - description: import an archive of scenes - /scenes/{scene}: - get: - description: get a scene's metadata - post: - description: creates a new scene using attached data - patch: - description: Edit scene's metadata - /scenes/{scene}/history: - get: - description: get a full history of a scene's modifications - post: - description: edit a scene's history - /scenes/{scene}/files: - get: - description: list all files in the scenes in their current state - /scenes/{scene}/permissions: + /scenes/{scene}/permissions: get: description: get scene permissions map patch: description: edit scene permission map - /tags: - get: - description: get a list of tags on this server - /tags/{tag}: - get: - description: get all scenes associated with this tag diff --git a/source/server/routes/api/v1/scenes/scene/permissions/get.test.ts b/source/server/routes/auth/access/get.test.ts similarity index 76% rename from source/server/routes/api/v1/scenes/scene/permissions/get.test.ts rename to source/server/routes/auth/access/get.test.ts index 6a901f3c..e463bdd9 100644 --- a/source/server/routes/api/v1/scenes/scene/permissions/get.test.ts +++ b/source/server/routes/auth/access/get.test.ts @@ -1,8 +1,8 @@ import { randomBytes } from "crypto"; import request from "supertest"; -import Vfs from "../../../../../../vfs/index.js"; -import User from "../../../../../../auth/User.js"; -import UserManager from "../../../../../../auth/UserManager.js"; +import User from "../../../auth/User.js"; +import UserManager from "../../../auth/UserManager.js"; +import Vfs from "../../../vfs/index.js"; @@ -10,7 +10,7 @@ import UserManager from "../../../../../../auth/UserManager.js"; * Minimal tests as most */ -describe("GET /api/v1/scenes/:scene/permissions", function(){ +describe("GET /auth/access/:scene", function(){ let vfs :Vfs, userManager :UserManager, user :User, admin :User, opponent :User; let titleSlug :string, scene_id :number; @@ -37,11 +37,11 @@ describe("GET /api/v1/scenes/:scene/permissions", function(){ await userManager.grant(titleSlug, "default", "none"); await userManager.grant(titleSlug, "any", "none"); //Anonymous - await request(this.server).get(`/api/v1/scenes/${titleSlug}/permissions`) + await request(this.server).get(`/auth/access/${titleSlug}`) .expect(404); //read-only User - await request(this.server).get(`/api/v1/scenes/${titleSlug}/permissions`) + await request(this.server).get(`/auth/access/${titleSlug}`) .auth(opponent.username, "12345678") .expect(404); }); -}); +}); \ No newline at end of file diff --git a/source/server/routes/api/v1/scenes/scene/permissions/get.ts b/source/server/routes/auth/access/get.ts similarity index 64% rename from source/server/routes/api/v1/scenes/scene/permissions/get.ts rename to source/server/routes/auth/access/get.ts index 3c96187e..00699703 100644 --- a/source/server/routes/api/v1/scenes/scene/permissions/get.ts +++ b/source/server/routes/auth/access/get.ts @@ -1,7 +1,6 @@ import { Request, Response } from "express"; -import { BadRequestError } from "../../../../../../utils/errors.js"; -import { getUserId, getUserManager } from "../../../../../../utils/locals.js"; +import { getUserManager } from "../../../utils/locals.js"; @@ -11,4 +10,4 @@ export default async function getPermissions(req :Request, res :Response){ let {scene} = req.params; let perms = await userManager.getPermissions(scene); res.status(200).send(perms); -}; +}; \ No newline at end of file diff --git a/source/server/routes/api/v1/scenes/scene/permissions/patch.test.ts b/source/server/routes/auth/access/patch.test.ts similarity index 81% rename from source/server/routes/api/v1/scenes/scene/permissions/patch.test.ts rename to source/server/routes/auth/access/patch.test.ts index e84dedcf..6b2de103 100644 --- a/source/server/routes/api/v1/scenes/scene/permissions/patch.test.ts +++ b/source/server/routes/auth/access/patch.test.ts @@ -1,8 +1,8 @@ import { randomBytes } from "crypto"; import request from "supertest"; -import Vfs from "../../../../../../vfs/index.js"; -import User from "../../../../../../auth/User.js"; -import UserManager from "../../../../../../auth/UserManager.js"; +import User from "../../../auth/User.js"; +import UserManager from "../../../auth/UserManager.js"; +import Vfs from "../../../vfs/index.js"; @@ -10,7 +10,7 @@ import UserManager from "../../../../../../auth/UserManager.js"; * Minimal tests as most */ -describe("PATCH /api/v1/scenes/:scene/permissions", function(){ +describe("PATCH /auth/access/:scene", function(){ let vfs :Vfs, userManager :UserManager, user :User, admin :User, opponent :User; let titleSlug :string, scene_id :number; @@ -34,7 +34,7 @@ describe("PATCH /api/v1/scenes/:scene/permissions", function(){ }); it("can change user permissions", async function(){ - await request(this.server).patch(`/api/v1/scenes/${titleSlug}/permissions`) + await request(this.server).patch(`/auth/access/${titleSlug}`) .auth(user.username, "12345678") .set("Content-Type", "application/json") .send({username: opponent.username, access: "write"}) @@ -48,7 +48,7 @@ describe("PATCH /api/v1/scenes/:scene/permissions", function(){ }); it("rejects invalid access levels", async function(){ - await request(this.server).patch(`/api/v1/scenes/${titleSlug}/permissions`) + await request(this.server).patch(`/auth/access/${titleSlug}`) .auth(user.username, "12345678") .set("Content-Type", "application/json") .send({username: opponent.username, access: "xxx"}) @@ -63,13 +63,13 @@ describe("PATCH /api/v1/scenes/:scene/permissions", function(){ it("requires admin access", async function(){ const body = {username: opponent.username, access: "admin"}; await userManager.grant(titleSlug, opponent.username, "write"); - await request(this.server).patch(`/api/v1/scenes/${titleSlug}/permissions`) + await request(this.server).patch(`/auth/access/${titleSlug}`) .auth(opponent.username, "12345678") .set("Content-Type", "application/json") .send(body) .expect(401); - let r = await request(this.server).patch(`/api/v1/scenes/${titleSlug}/permissions`) + let r = await request(this.server).patch(`/auth/access/${titleSlug}`) .auth(user.username, "12345678") .set("Content-Type", "application/json") .send(body) @@ -77,4 +77,4 @@ describe("PATCH /api/v1/scenes/:scene/permissions", function(){ expect(await userManager.getAccessRights(titleSlug, opponent.uid)).to.equal("admin"); }); -}); +}); \ No newline at end of file diff --git a/source/server/routes/api/v1/scenes/scene/permissions/patch.ts b/source/server/routes/auth/access/patch.ts similarity index 81% rename from source/server/routes/api/v1/scenes/scene/permissions/patch.ts rename to source/server/routes/auth/access/patch.ts index 053140c5..c4f58719 100644 --- a/source/server/routes/api/v1/scenes/scene/permissions/patch.ts +++ b/source/server/routes/auth/access/patch.ts @@ -1,6 +1,6 @@ import { Request, Response } from "express"; -import { getUserManager } from "../../../../../../utils/locals.js"; +import { getUserManager } from "../../../utils/locals.js"; @@ -11,4 +11,4 @@ export default async function patchPermissions(req :Request, res :Response){ let {username, access} = req.body; await userManager.grant(scene, username, access); res.status(204).send(); -}; +}; \ No newline at end of file diff --git a/source/server/routes/auth/index.ts b/source/server/routes/auth/index.ts new file mode 100644 index 00000000..fa16c69c --- /dev/null +++ b/source/server/routes/auth/index.ts @@ -0,0 +1,49 @@ + +import { Router } from "express"; +import { rateLimit } from 'express-rate-limit' +import bodyParser from "body-parser"; + +import { canAdmin, canRead, either, isAdministrator } from "../../utils/locals.js"; +import wrap from "../../utils/wrapAsync.js"; +import { getLogin, getLoginLink, sendLoginLink, postLogin } from "./login.js"; +import { postLogout } from "./logout.js"; +import getPermissions from "./access/get.js"; +import patchPermissions from "./access/patch.js"; + +const useJSON = bodyParser.json(); + +const router = Router(); + +/** Configure cache behaviour for the whole API + * Settings can be changed individually further down the line + */ +router.use((req, res, next)=>{ + //Browser should always make the request + res.set("Cache-Control", "no-cache"); + next(); +}); + + +router.use("/login", (req, res, next)=>{ + res.append("Cache-Control", "private"); + next(); +}); +router.get("/login", wrap(getLogin)); +router.post("/login", useJSON, postLogin); +router.get("/login/:username/link", isAdministrator, wrap(getLoginLink)); +router.post("/login/:username/link", either(isAdministrator, rateLimit({ + //Special case of real low rate-limiting for non-admin users to send emails + windowMs: 1 * 60 * 1000, // 1 minute + limit: 1, // Limit each IP to 1 request per `window`. + standardHeaders: 'draft-7', + legacyHeaders: false, +})), wrap(sendLoginLink)); + +router.post("/logout", postLogout); + + +router.get("/access/:scene", canRead, wrap(getPermissions)); +router.patch("/access/:scene", canAdmin, useJSON, wrap(patchPermissions)); + + +export default router; diff --git a/source/server/routes/api/v1/login.test.ts b/source/server/routes/auth/login.test.ts similarity index 81% rename from source/server/routes/api/v1/login.test.ts rename to source/server/routes/auth/login.test.ts index c0293dad..829b5433 100644 --- a/source/server/routes/api/v1/login.test.ts +++ b/source/server/routes/auth/login.test.ts @@ -1,12 +1,12 @@ import request from "supertest"; -import Vfs from "../../../vfs/index.js"; -import User from "../../../auth/User.js"; -import UserManager from "../../../auth/UserManager.js"; +import Vfs from "../../vfs/index.js"; +import User from "../../auth/User.js"; +import UserManager from "../../auth/UserManager.js"; -describe("/api/v1/login", function(){ +describe("/auth/login", function(){ let vfs :Vfs, userManager :UserManager, user :User, admin :User; this.beforeEach(async function(){ let locals = await createIntegrationContext(this); @@ -21,7 +21,7 @@ describe("/api/v1/login", function(){ it("sets a cookie", async function(){ this.agent = request.agent(this.server); - await this.agent.post("/api/v1/login") + await this.agent.post("/auth/login") .send({username: user.username, password: "12345678"}) .set("Content-Type", "application/json") .set("Accept", "") @@ -30,7 +30,7 @@ describe("/api/v1/login", function(){ }); it("can get login status (not connected)", async function(){ - await request(this.server).get("/api/v1/login") + await request(this.server).get("/auth/login") .set("Accept", "application/json") .expect(200) .expect({isAdministrator:false, isDefaultUser: true}); @@ -38,12 +38,12 @@ describe("/api/v1/login", function(){ it("can get login status (connected)", async function(){ this.agent = request.agent(this.server); - await this.agent.post("/api/v1/login") + await this.agent.post("/auth/login") .send({username: user.username, password: "12345678"}) .set("Content-Type", "application/json") .set("Accept", "") .expect(200); - await this.agent.get("/api/v1/login") + await this.agent.get("/auth/login") .set("Accept", "application/json") .expect(200) .expect({ @@ -56,12 +56,12 @@ describe("/api/v1/login", function(){ it("can get login status (admin)", async function(){ this.agent = request.agent(this.server); - await this.agent.post("/api/v1/login") + await this.agent.post("/auth/login") .send({username: admin.username, password: "12345678"}) .set("Content-Type", "application/json") .set("Accept", "") .expect(200); - await this.agent.get("/api/v1/login") + await this.agent.get("/auth/login") .set("Accept", "application/json") .expect(200) .expect({ @@ -74,7 +74,7 @@ describe("/api/v1/login", function(){ it("send a proper error if username is missing", async function(){ this.agent = request.agent(this.server); - let res = await this.agent.post("/api/v1/login") + let res = await this.agent.post("/auth/login") .send({/*no username */ password: "12345678"}) .set("Content-Type", "application/json") .set("Accept", "") @@ -84,7 +84,7 @@ describe("/api/v1/login", function(){ it("send a proper error if password is missing", async function(){ this.agent = request.agent(this.server); - let res = await this.agent.post("/api/v1/login") + let res = await this.agent.post("/auth/login") .send({username: user.username /*no password */}) .set("Content-Type", "application/json") .set("Accept", "") @@ -94,17 +94,17 @@ describe("/api/v1/login", function(){ it("can logout", async function(){ let agent = request.agent(this.server); - await agent.post("/api/v1/login") + await agent.post("/auth/login") .send({username: user.username, password: "12345678"}) .set("Content-Type", "application/json") .set("Accept", "") .expect(200) .expect('set-cookie', /session=/); - await agent.post("/api/v1/logout") + await agent.post("/auth/logout") .expect(200); - await agent.get("/api/v1/login") + await agent.get("/auth/login") .expect(200) .expect({ isDefaultUser: true, @@ -121,14 +121,14 @@ describe("/api/v1/login", function(){ }; //Manually build the header - await request(this.server).get("/api/v1/login") + await request(this.server).get("/auth/login") .set("Authorization", `Basic ${Buffer.from(`${user.username}:12345678`).toString("base64")}`) .set("Accept", "application/json") .expect(200) .expect(res); //make supertest build the header - await request(this.server).get("/api/v1/login") + await request(this.server).get("/auth/login") .auth(user.username, "12345678") .set("Accept", "application/json") .expect(200) @@ -137,7 +137,7 @@ describe("/api/v1/login", function(){ it("rejects bad header", async function(){ // Missing the "Basic " part - let res = await request(this.server).get("/api/v1/login") + let res = await request(this.server).get("/auth/login") .set("Authorization", `${Buffer.from(`${user.username}:12345678`).toString("base64")}`) .expect(200); //Still answers 200, but no login data @@ -145,7 +145,7 @@ describe("/api/v1/login", function(){ }); it("rejects bad user:password", async function(){ // Missing the "Basic " part - await request(this.server).get("/api/v1/login") + await request(this.server).get("/auth/login") .auth(user.username, "badPassword") .expect(401); }) diff --git a/source/server/routes/api/v1/login.ts b/source/server/routes/auth/login.ts similarity index 89% rename from source/server/routes/api/v1/login.ts rename to source/server/routes/auth/login.ts index f9493a39..2dbc74dd 100644 --- a/source/server/routes/api/v1/login.ts +++ b/source/server/routes/auth/login.ts @@ -1,9 +1,9 @@ import { createHmac } from "crypto"; import { Request, RequestHandler, Response } from "express"; -import User, { SafeUser } from "../../../auth/User.js"; -import { BadRequestError, ForbiddenError, NotFoundError, UnauthorizedError } from "../../../utils/errors.js"; -import { AppLocals, getHost, getLocals, getUser, getUserManager } from "../../../utils/locals.js"; -import sendmail from "../../../utils/mails/send.js"; +import User, { SafeUser } from "../../auth/User.js"; +import { BadRequestError, ForbiddenError, NotFoundError, UnauthorizedError } from "../../utils/errors.js"; +import { AppLocals, getHost, getLocals, getUser, getUserManager } from "../../utils/locals.js"; +import sendmail from "../../utils/mails/send.js"; /** * * @type {RequestHandler} @@ -15,7 +15,7 @@ export const postLogin :RequestHandler = (req, res, next)=>{ if(!password) throw new BadRequestError("password not provided"); userManager.getUserByNamePassword(username, password).then(user=>{ let safeUser = User.safe(user); - Object.assign(req.session as any, safeUser); + Object.assign((req as any).session as any, safeUser); res.status(200).send({...safeUser, code: 200, message: "OK"}); }, (e)=>{ @@ -30,7 +30,7 @@ export const postLogin :RequestHandler = (req, res, next)=>{ export async function getLogin(req :Request, res:Response){ let {payload, sig, redirect} = req.query; if(typeof payload !== "string" || !payload || !sig){ - return res.status(200).send(User.safe(req.session as any)); + return res.status(200).send(User.safe((req as any).session as any)); } let userManager = getUserManager(req); @@ -53,7 +53,7 @@ export async function getLogin(req :Request, res:Response){ console.log((e as any).message); throw new BadRequestError(`Failed to parse login payload`); } - Object.assign(req.session as any, User.safe(user)); + Object.assign((req as any).session as any, User.safe(user)); if(redirect && typeof redirect === "string"){ return res.redirect(302, redirect ); } @@ -79,7 +79,7 @@ function makeLoginLink(user :User, key :string){ } function makeRedirect(opts:ReturnType, redirect :URL) :URL{ - let url = new URL("/api/v1/login", redirect.toString()); + let url = new URL("/auth/login", redirect.toString()); url.searchParams.set("payload", opts.params); url.searchParams.set("sig", opts.sig); url.searchParams.set("redirect", redirect.pathname); diff --git a/source/server/routes/api/v1/logout.ts b/source/server/routes/auth/logout.ts similarity index 100% rename from source/server/routes/api/v1/logout.ts rename to source/server/routes/auth/logout.ts diff --git a/source/server/routes/api/v1/scenes/scene/history/get.test.ts b/source/server/routes/history/get.test.ts similarity index 81% rename from source/server/routes/api/v1/scenes/scene/history/get.test.ts rename to source/server/routes/history/get.test.ts index 11166fb5..752de39a 100644 --- a/source/server/routes/api/v1/scenes/scene/history/get.test.ts +++ b/source/server/routes/history/get.test.ts @@ -1,8 +1,8 @@ import request from "supertest"; -import Vfs from "../../../../../../vfs/index.js"; -import User from "../../../../../../auth/User.js"; -import UserManager from "../../../../../../auth/UserManager.js"; +import Vfs from "../../vfs/index.js"; +import User from "../../auth/User.js"; +import UserManager from "../../auth/UserManager.js"; @@ -10,7 +10,7 @@ import UserManager from "../../../../../../auth/UserManager.js"; * Minimal tests as most */ -describe("GET /api/v1/scenes/:scene/history", function(){ +describe("GET /history/:scene", function(){ let vfs :Vfs, userManager :UserManager, user :User, admin :User, opponent :User; describe("with sample data", function(){ @@ -46,7 +46,7 @@ describe("GET /api/v1/scenes/:scene/history", function(){ }); it("get a scene's history", async function(){ - let res = await request(this.server).get("/api/v1/scenes/foo/history") + let res = await request(this.server).get("/history/foo") .set("Accept", "application/json") .expect(200) .expect("Content-Type", "application/json; charset=utf-8"); @@ -62,7 +62,7 @@ describe("GET /api/v1/scenes/:scene/history", function(){ }); it("get text history", async function(){ - let res = await request(this.server).get("/api/v1/scenes/foo/history") + let res = await request(this.server).get("/history/foo") .set("Accept", "text/plain") .expect(200) .expect("Content-Type", "text/plain; charset=utf-8"); @@ -71,7 +71,7 @@ describe("GET /api/v1/scenes/:scene/history", function(){ it("get an empty history", async function(){ await vfs.createScene("empty", user.uid); - let res = await request(this.server).get("/api/v1/scenes/empty/history") + let res = await request(this.server).get("/history/empty") .expect(200); }) @@ -82,12 +82,12 @@ describe("GET /api/v1/scenes/:scene/history", function(){ await userManager.grant("private", "any", "none"); }); it("(anonymous)", async function(){ - await request(this.server).get("/api/v1/scenes/private/history") + await request(this.server).get("/history/private") .expect(404); }); it("(user)", async function(){ - await request(this.server).get("/api/v1/scenes/private/history") + await request(this.server).get("/history/private") .auth(opponent.username, "12345678") .expect(404); }); diff --git a/source/server/routes/api/v1/scenes/scene/history/get.ts b/source/server/routes/history/get.ts similarity index 86% rename from source/server/routes/api/v1/scenes/scene/history/get.ts rename to source/server/routes/history/get.ts index 31a45fa0..72e2174c 100644 --- a/source/server/routes/api/v1/scenes/scene/history/get.ts +++ b/source/server/routes/history/get.ts @@ -1,6 +1,6 @@ import { Request, Response } from "express"; -import { getVfs } from "../../../../../../utils/locals.js"; +import { getVfs } from "../../utils/locals.js"; export default async function getSceneHistory(req :Request, res :Response){ diff --git a/source/server/routes/history/index.ts b/source/server/routes/history/index.ts new file mode 100644 index 00000000..516f7945 --- /dev/null +++ b/source/server/routes/history/index.ts @@ -0,0 +1,28 @@ + +import { Router } from "express"; +import { canAdmin, canRead } from "../../utils/locals.js"; +import wrap from "../../utils/wrapAsync.js"; + +import bodyParser from "body-parser"; + + +import { postSceneHistory } from "./post.js"; +import getSceneHistory from "./get.js"; + +const router = Router(); + +/** Configure cache behaviour for the whole API + * Settings can be changed individually further down the line + */ +router.use((req, res, next)=>{ + //Browser should always make the request + res.set("Cache-Control", "no-cache"); + next(); +}); + +router.use("/:scene", canRead); + +router.get("/:scene", wrap(getSceneHistory)); +router.post("/:scene", canAdmin, bodyParser.json(), wrap(postSceneHistory)); + +export default router; diff --git a/source/server/routes/api/v1/scenes/scene/history/post.test.ts b/source/server/routes/history/post.test.ts similarity index 88% rename from source/server/routes/api/v1/scenes/scene/history/post.test.ts rename to source/server/routes/history/post.test.ts index df71d646..79238793 100644 --- a/source/server/routes/api/v1/scenes/scene/history/post.test.ts +++ b/source/server/routes/history/post.test.ts @@ -1,15 +1,15 @@ import request from "supertest"; -import Vfs from "../../../../../../vfs/index.js"; -import User from "../../../../../../auth/User.js"; -import UserManager from "../../../../../../auth/UserManager.js"; +import Vfs from "../../vfs/index.js"; +import User from "../../auth/User.js"; +import UserManager from "../../auth/UserManager.js"; import { randomBytes } from "crypto"; -describe("POST /api/v1/scenes/:scene/history", function(){ +describe("POST /history/:scene", function(){ let vfs :Vfs, userManager :UserManager, user :User, admin :User; let titleSlug :string, scene_id :number; @@ -53,7 +53,7 @@ describe("POST /api/v1/scenes/:scene/history", function(){ let {data} = await vfs.getDocById(point); await antidate(); - let res = await request(this.server).post(`/api/v1/scenes/${titleSlug}/history`) + let res = await request(this.server).post(`/history/${titleSlug}`) .auth("bob", "12345678") .set("Content-Type", "application/json") .send({type: "document", id: point }) @@ -76,7 +76,7 @@ describe("POST /api/v1/scenes/:scene/history", function(){ await vfs.writeDoc(`{"id": 2}`, scene_id); await vfs.writeFile(dataStream(["world"]), {mime: "text/html", name:"articles/hello.txt", scene: scene_id, user_id: user.uid }); - let res = await request(this.server).post(`/api/v1/scenes/${titleSlug}/history`) + let res = await request(this.server).post(`/history/${titleSlug}`) .auth("bob", "12345678") .set("Content-Type", "application/json") .send({name: ref.name, generation: ref.generation }) @@ -95,7 +95,7 @@ describe("POST /api/v1/scenes/:scene/history", function(){ await vfs.writeFile(dataStream(["hello"]), {mime: "text/html", name:"articles/hello.txt", scene: scene_id, user_id: user.uid }); let ref = await vfs.getDocById(id); - let res = await request(this.server).post(`/api/v1/scenes/${titleSlug}/history`) + let res = await request(this.server).post(`/history/${titleSlug}`) .auth("bob", "12345678") .set("Content-Type", "application/json") .send({type: "document", id }) @@ -125,7 +125,7 @@ describe("POST /api/v1/scenes/:scene/history", function(){ expect(allFiles).to.have.property("length", 1); expect(allFiles[0]).to.have.property("hash" ).ok; - let res = await request(this.server).post(`/api/v1/scenes/${titleSlug}/history`) + let res = await request(this.server).post(`/history/${titleSlug}`) .auth("bob", "12345678") .set("Content-Type", "application/json") .send({type: "file", id: ref }) @@ -147,7 +147,7 @@ describe("POST /api/v1/scenes/:scene/history", function(){ await antidate(); await vfs.writeDoc(`{"id": 1}`, scene_id); - let res = await request(this.server).post(`/api/v1/scenes/${titleSlug}/history`) + let res = await request(this.server).post(`/history/${titleSlug}`) .auth("bob", "12345678") .set("Content-Type", "application/json") .send({type: "file", id: ref.id }) @@ -167,7 +167,7 @@ describe("POST /api/v1/scenes/:scene/history", function(){ ]; for(let body of bodies){ - let res = await request(this.server).post(`/api/v1/scenes/${titleSlug}/history`) + let res = await request(this.server).post(`/history/${titleSlug}`) .auth("bob", "12345678") .set("Content-Type", "application/json") .send(body) @@ -185,7 +185,7 @@ describe("POST /api/v1/scenes/:scene/history", function(){ it("requires admin rights over the scene", async function(){ const oscar = await userManager.addUser("oscar", "12345678"); //Fails with read-only - await request(this.server).post(`/api/v1/scenes/${titleSlug}/history`) + await request(this.server).post(`/history/${titleSlug}`) .auth("oscar", "12345678") .set("Content-Type", "application/json") .send({id: docId, type: "document" }) @@ -194,7 +194,7 @@ describe("POST /api/v1/scenes/:scene/history", function(){ await userManager.grant(titleSlug, "oscar", "write"); //Fails with write access - await request(this.server).post(`/api/v1/scenes/${titleSlug}/history`) + await request(this.server).post(`/history/${titleSlug}`) .auth("oscar", "12345678") .set("Content-Type", "application/json") .send({id: docId, type: "document" }) @@ -204,7 +204,7 @@ describe("POST /api/v1/scenes/:scene/history", function(){ await userManager.grant(titleSlug, "oscar", "admin"); //Succeeds with admin access - await request(this.server).post(`/api/v1/scenes/${titleSlug}/history`) + await request(this.server).post(`/history/${titleSlug}`) .auth("oscar", "12345678") .set("Content-Type", "application/json") .send({id: docId, type: "document" }) @@ -213,7 +213,7 @@ describe("POST /api/v1/scenes/:scene/history", function(){ }); it("admins can always restore scenes", async function(){ - await request(this.server).post(`/api/v1/scenes/${titleSlug}/history`) + await request(this.server).post(`/history/${titleSlug}`) .auth("alice", "12345678") .set("Content-Type", "application/json") .send({id: docId, type: "document" }) diff --git a/source/server/routes/api/v1/scenes/scene/history/post.ts b/source/server/routes/history/post.ts similarity index 90% rename from source/server/routes/api/v1/scenes/scene/history/post.ts rename to source/server/routes/history/post.ts index 971d93c7..0971b9cb 100644 --- a/source/server/routes/api/v1/scenes/scene/history/post.ts +++ b/source/server/routes/history/post.ts @@ -1,14 +1,14 @@ import { Request, Response } from "express"; -import path from "path"; -import { BadRequestError } from "../../../../../../utils/errors.js"; -import { getUser, getVfs } from "../../../../../../utils/locals.js"; -import { ItemEntry } from "../../../../../../vfs/index.js"; + +import { BadRequestError } from "../../utils/errors.js"; +import { getUser, getVfs } from "../../utils/locals.js"; +import { ItemEntry } from "../../vfs/index.js"; /** * Restore a scene's history to just after some point. * - * What is "before" or "after" is defined by the reverse of what is returned by `GET /api/v1/scenes/:scene/history` + * What is "before" or "after" is defined by the reverse of what is returned by `GET /scenes/:scene/history` * That is the algorithm will remove everything in indices : * history[0] .. history[indexOf(:id)] * diff --git a/source/server/server.ts b/source/server/routes/index.ts similarity index 89% rename from source/server/server.ts rename to source/server/routes/index.ts index 99ce66fd..cedb119b 100644 --- a/source/server/server.ts +++ b/source/server/routes/index.ts @@ -3,18 +3,19 @@ import path from "path"; import util from "util"; import cookieSession from "cookie-session"; import express from "express"; +import bodyParser from "body-parser"; -import UserManager from "./auth/UserManager.js"; -import { BadRequestError, HTTPError } from "./utils/errors.js"; +import UserManager from "../auth/UserManager.js"; +import { BadRequestError, HTTPError } from "../utils/errors.js"; import { mkdir } from "fs/promises"; -import {AppLocals, canRead, canWrite, getHost, getUserManager, isUser} from "./utils/locals.js"; +import {AppLocals, canRead, canWrite, getHost, getUserManager, isUser} from "../utils/locals.js"; -import openDatabase from "./vfs/helpers/db.js"; -import Vfs from "./vfs/index.js"; -import defaultConfig from "./utils/config.js"; -import User from "./auth/User.js"; -import Templates from "./utils/templates.js"; +import openDatabase from "../vfs/helpers/db.js"; +import Vfs from "../vfs/index.js"; +import defaultConfig from "../utils/config.js"; +import User from "../auth/User.js"; +import Templates from "../utils/templates.js"; export default async function createServer(config = defaultConfig) :Promise{ @@ -186,7 +187,7 @@ export default async function createServer(config = defaultConfig) :Promise`id=${id}`).join("&")}`) + let r = await request(this.server).get(`/scenes?${scenes.map(id=>`id=${id}`).join("&")}`) .set("Accept", "application/json") .send({scenes: scenes}) .expect(200) @@ -121,7 +119,7 @@ describe("GET /api/v1/scenes", function(){ let scene :any = await vfs.getScene("write", user.uid); delete scene.thumb; - let r = await request(this.server).get(`/api/v1/scenes?access=write`) + let r = await request(this.server).get(`/scenes?access=write`) .auth(user.username, "12345678") .set("Accept", "application/json") .send({scenes: scenes}) @@ -144,7 +142,7 @@ describe("GET /api/v1/scenes", function(){ delete s1.thumb; delete s2.thumb; - let r = await request(this.server).get(`/api/v1/scenes?access=write&access=admin`) + let r = await request(this.server).get(`/scenes?access=write&access=admin`) .auth(user.username, "12345678") .set("Accept", "application/json") .send({scenes: scenes}) @@ -173,7 +171,7 @@ describe("GET /api/v1/scenes", function(){ mtime: mtime.toISOString(), ctime: ctime.toISOString() })); - let r = await request(this.server).get(`/api/v1/scenes?match=e`) + let r = await request(this.server).get(`/scenes?match=e`) .auth(user.username, "12345678") .set("Accept", "application/json") .send({scenes: scenes}) @@ -197,7 +195,7 @@ describe("GET /api/v1/scenes", function(){ }); it("use default limit", async function(){ - let r = await request(this.server).get(`/api/v1/scenes`) + let r = await request(this.server).get(`/scenes`) .set("Accept", "application/json") .expect(200) .expect("Content-Type", "application/json; charset=utf-8"); @@ -205,7 +203,7 @@ describe("GET /api/v1/scenes", function(){ }); it("use custom limit and offset", async function(){ - let r = await request(this.server).get(`/api/v1/scenes?limit=12&offset=12&match=scene_`) + let r = await request(this.server).get(`/scenes?limit=12&offset=12&match=scene_`) .set("Accept", "application/json") .expect(200) .expect("Content-Type", "application/json; charset=utf-8"); @@ -228,7 +226,7 @@ describe("GET /api/v1/scenes", function(){ }); it("can get only archived scenes", async function(){ - let r = await request(this.server).get("/api/v1/scenes?access=none") + let r = await request(this.server).get("/scenes?access=none") .auth(admin.username, "12345678") .expect(200); let names = r.body.scenes.map((s:any)=>s.name) @@ -238,14 +236,14 @@ describe("GET /api/v1/scenes", function(){ it it("requires global admin rights", async function(){ - let r = await request(this.server).get("/api/v1/scenes?access=none") + let r = await request(this.server).get("/scenes?access=none") .auth(user.username, "12345678") .expect(200); expect(r.body).to.have.property("scenes").to.have.length(0); }); it("won't return archived scenes in a default query", async function(){ - let r = await request(this.server).get("/api/v1/scenes") + let r = await request(this.server).get("/scenes") .auth(user.username, "12345678") .expect(200); let names = r.body.scenes.map((s:any)=>s.name) diff --git a/source/server/routes/api/v1/scenes/get.ts b/source/server/routes/scenes/get.ts similarity index 92% rename from source/server/routes/api/v1/scenes/get.ts rename to source/server/routes/scenes/get.ts index c791d039..501e5300 100644 --- a/source/server/routes/api/v1/scenes/get.ts +++ b/source/server/routes/scenes/get.ts @@ -2,13 +2,14 @@ import { createHash } from "crypto"; import { Request, Response } from "express"; import path from "path"; -import { AccessType } from "../../../../auth/UserManager.js"; -import { HTTPError } from "../../../../utils/errors.js"; -import { getHost, getUser, getVfs } from "../../../../utils/locals.js"; -import { wrapFormat } from "../../../../utils/wrapAsync.js"; -import { ZipEntry, zip } from "../../../../utils/zip/index.js"; import { once } from "events"; +import { AccessType } from "../../auth/UserManager.js"; +import { HTTPError } from "../../utils/errors.js"; +import { getVfs, getUser, getHost } from "../../utils/locals.js"; +import { wrapFormat } from "../../utils/wrapAsync.js"; +import { ZipEntry, zip } from "../../utils/zip/index.js"; + export default async function getScenes(req :Request, res :Response){ let vfs = getVfs(req); let u = getUser(req); diff --git a/source/server/routes/scenes/index.ts b/source/server/routes/scenes/index.ts index 11fc1048..6ac4e31b 100644 --- a/source/server/routes/scenes/index.ts +++ b/source/server/routes/scenes/index.ts @@ -2,21 +2,31 @@ import { Router } from "express"; import bodyParser from "body-parser"; -import {handlePropfind} from "./propfind.js"; -import {handlePutFile, handlePutDocument} from "./put/index.js"; import { canAdmin, canRead, canWrite, isAdministrator, isUser } from "../../utils/locals.js"; import wrap from "../../utils/wrapAsync.js"; -import handleGetDocument from "./get/document.js"; -import handleGetFile from "./get/file.js"; -import handleMoveFile from "./move/file.js"; -import handleDeleteFile from "./delete/file.js"; -import handleCopyFile from "./copy/file.js"; -import handleCopyDocument from "./copy/document.js"; -import handleDeleteScene from "./delete/scene.js"; -import handleCreateFolder from "./mkcol/folder.js"; -import handleCreateScene from "./mkcol/scene.js"; +import getScenes from "./get.js"; +import {handlePropfind} from "./propfind.js"; +import handlePostScene from "./post.js"; + +import handleDeleteScene from "./scene/delete.js"; +import handleCreateScene from "./scene/mkcol.js"; +import getScene from "./scene/get.js"; +import patchScene from "./scene/patch.js"; + +import handleCopyDocument from "./scene/files/copy/document.js"; +import handleCopyFile from "./scene/files/copy/file.js"; +import handleDeleteFile from "./scene/files/delete/file.js"; +import handleGetDocument from "./scene/files/get/document.js"; +import handleGetFile from "./scene/files/get/file.js"; +import handleMoveFile from "./scene/files/move/file.js"; +import handlePutDocument from "./scene/files/put/document.js"; +import handlePutFile from "./scene/files/put/file.js"; +import handleCreateFolder from "./scene/files/mkcol/folder.js"; +import postScene from "./scene/post.js"; + + const router = Router(); /** Configure cache behaviour for everything under `/scenes/**` @@ -28,7 +38,12 @@ router.use((req, res, next)=>{ next(); }); +router.get("/", wrap(getScenes)); router.propfind("/", wrap(handlePropfind)); +router.post("/", isAdministrator, wrap(handlePostScene)); + +//allow POST outside of canRead : overwrite permissions are otherwise checked +router.post("/:scene", wrap(postScene)); //Allow mkcol outside of canRead check router.mkcol(`/:scene`, wrap(handleCreateScene)); @@ -37,7 +52,10 @@ router.mkcol(`/:scene`, wrap(handleCreateScene)); * Protect everything after this with canRead handler */ router.use("/:scene", canRead); + +router.get("/:scene", wrap(getScene)); router.propfind("/:scene", wrap(handlePropfind)); +router.patch("/:scene", canAdmin, bodyParser.json(), wrap(patchScene)); router.delete("/:scene", canAdmin, wrap(handleDeleteScene)); diff --git a/source/server/routes/api/v1/scenes/post.test.ts b/source/server/routes/scenes/post.test.ts similarity index 85% rename from source/server/routes/api/v1/scenes/post.test.ts rename to source/server/routes/scenes/post.test.ts index e7740250..48c8613c 100644 --- a/source/server/routes/api/v1/scenes/post.test.ts +++ b/source/server/routes/scenes/post.test.ts @@ -2,9 +2,10 @@ import fs from "fs/promises"; import request from "supertest"; -import Vfs from "../../../../vfs/index.js"; -import UserManager from "../../../../auth/UserManager.js"; -import { NotFoundError } from "../../../../utils/errors.js"; +import UserManager from "../../auth/UserManager.js"; +import { NotFoundError } from "../../utils/errors.js"; +import Vfs from "../../vfs/index.js"; + function binaryParser(res:request.Response, callback:(err:Error|null, data:Buffer)=>any) { res.setEncoding('binary'); @@ -17,7 +18,7 @@ function binaryParser(res:request.Response, callback:(err:Error|null, data:Buffe }); } -describe("POST /api/v1/scenes", function(){ +describe("POST /scenes", function(){ let vfs:Vfs, userManager:UserManager, ids :number[]; this.beforeEach(async function(){ let locals = await createIntegrationContext(this); @@ -31,14 +32,14 @@ describe("POST /api/v1/scenes", function(){ it("requires admin rights", async function(){ await userManager.addUser("bob", "12345678", false); - await request(this.server).post("/api/v1/scenes") + await request(this.server).post("/scenes") .auth("bob", "12345678") .send("xxx") //don't care .expect(401); }); it("can import a zip of exported scenes", async function(){ - //Where scenes are exported from the `GET /api/v1/scenes` endpoint + //Where scenes are exported from the `GET /scenes` endpoint const user = await userManager.addUser("alice", "12345678", true); await vfs.createScene("foo", user.uid); await vfs.writeDoc(`{"id":1}`, "foo", user.uid); @@ -48,7 +49,7 @@ describe("POST /api/v1/scenes", function(){ await vfs.writeDoc(`{"id":2}`, "bar", user.uid); await vfs.writeFile(dataStream(), {scene:"bar", name:"articles/hello.html", mime: "text/html", user_id:user.uid}); - let zip = await request(this.server).get("/api/v1/scenes") + let zip = await request(this.server).get("/scenes") .auth("alice", "12345678") .set("Accept", "application/zip") .parse(binaryParser) @@ -63,7 +64,7 @@ describe("POST /api/v1/scenes", function(){ await vfs.removeScene("bar"); await expect(vfs.getScene("bar")).to.be.rejectedWith(NotFoundError); - let res = await request(this.server).post("/api/v1/scenes") + let res = await request(this.server).post("/scenes") .auth("alice", "12345678") .set("Content-Type", "application/zip") .send(zip.body) @@ -77,12 +78,12 @@ describe("POST /api/v1/scenes", function(){ }); it("can import a scene zip", async function(){ - //Where scene is exported from the `GET /api/v1/scenes/{id}` endpoint + //Where scene is exported from the `GET /scenes/{id}` endpoint const user = await userManager.addUser("alice", "12345678", true); await vfs.createScene("foo", user.uid); await vfs.writeDoc(`{"id":1}`, "foo", user.uid); await vfs.writeFile(dataStream(), {scene:"foo", name:"articles/hello.html", mime: "text/html", user_id:user.uid}); - let zip = await request(this.server).get("/api/v1/scenes/foo") + let zip = await request(this.server).get("/scenes/foo") .auth("alice", "12345678") .set("Accept", "application/zip") .parse(binaryParser) @@ -93,7 +94,7 @@ describe("POST /api/v1/scenes", function(){ await vfs.removeScene("foo"); await expect(vfs.getFileProps({scene:"foo", name:"articles/hello.html"})).to.be.rejectedWith(NotFoundError); - let res = await request(this.server).post("/api/v1/scenes") + let res = await request(this.server).post("/scenes") .auth("alice", "12345678") .set("Content-Type", "application/zip") .send(zip.body) diff --git a/source/server/routes/api/v1/scenes/post.ts b/source/server/routes/scenes/post.ts similarity index 88% rename from source/server/routes/api/v1/scenes/post.ts rename to source/server/routes/scenes/post.ts index aa8a2e0f..e0b4180d 100644 --- a/source/server/routes/api/v1/scenes/post.ts +++ b/source/server/routes/scenes/post.ts @@ -1,14 +1,14 @@ import fs from "fs/promises"; import path from "path"; -import {once} from "events"; import { Request, Response } from "express"; -import { getHost, getUser, getVfs } from "../../../../utils/locals.js"; -import { Uid } from "../../../../utils/uid.js"; -import { unzip } from "../../../../utils/zip/index.js"; -import { HTTPError } from "../../../../utils/errors.js"; import { createReadStream } from "fs"; -import { getMimeType } from "../../../../utils/filetypes.js"; + +import { HTTPError } from "../../utils/errors.js"; +import { getMimeType } from "../../utils/filetypes.js"; +import { getVfs, getUser } from "../../utils/locals.js"; +import { Uid } from "../../utils/uid.js"; +import { unzip } from "../../utils/zip/index.js"; interface ImportResults { diff --git a/source/server/routes/scenes/delete/scene.test.ts b/source/server/routes/scenes/scene/delete.test.ts similarity index 99% rename from source/server/routes/scenes/delete/scene.test.ts rename to source/server/routes/scenes/scene/delete.test.ts index dc6ba176..dceccb84 100644 --- a/source/server/routes/scenes/delete/scene.test.ts +++ b/source/server/routes/scenes/scene/delete.test.ts @@ -4,8 +4,9 @@ import { expect } from "chai"; import User from "../../../auth/User.js"; import UserManager from "../../../auth/UserManager.js"; -import Vfs from "../../../vfs/index.js"; import { NotFoundError } from "../../../utils/errors.js"; +import Vfs from "../../../vfs/index.js"; + diff --git a/source/server/routes/scenes/delete/scene.ts b/source/server/routes/scenes/scene/delete.ts similarity index 100% rename from source/server/routes/scenes/delete/scene.ts rename to source/server/routes/scenes/scene/delete.ts diff --git a/source/server/routes/scenes/copy/copy.test.ts b/source/server/routes/scenes/scene/files/copy/copy.test.ts similarity index 80% rename from source/server/routes/scenes/copy/copy.test.ts rename to source/server/routes/scenes/scene/files/copy/copy.test.ts index 9a147011..4f2d0e3f 100644 --- a/source/server/routes/scenes/copy/copy.test.ts +++ b/source/server/routes/scenes/scene/files/copy/copy.test.ts @@ -1,15 +1,10 @@ -import fs from "fs/promises"; -import path from "path"; -import {tmpdir} from "os"; - import request from "supertest"; import { expect } from "chai"; -import express, { Application } from "express"; -import User from "../../../auth/User.js"; -import UserManager from "../../../auth/UserManager.js"; -import Vfs, { WriteFileParams } from "../../../vfs/index.js"; -import { Element, xml2js } from "xml-js"; +import User from "../../../../../auth/User.js"; +import UserManager from "../../../../../auth/UserManager.js"; +import Vfs from "../../../../../vfs/index.js"; +import { WriteFileParams } from "../../../../../vfs/types.js"; @@ -28,7 +23,7 @@ describe("COPY /scenes/:name", function(){ this.agent = request.agent(this.server); - await this.agent.post("/api/v1/login") + await this.agent.post("/auth/login") .send({username: user.username, password: "12345678"}) .set("Content-Type", "application/json") .set("Accept", "") @@ -72,10 +67,7 @@ describe("COPY /scenes/:name", function(){ it("can COPY a document", async function(){ let {id:scene_id} = await vfs.getScene("foo"); await vfs.writeDoc('{"id":2}', "foo", user.uid); - let {ctime:t1, id, generation, ...src} = await vfs.getDoc(scene_id); - //Round mtime down, otherwise sqlite's conversion might round to another second. - src.mtime.setMilliseconds(0); - await vfs._db.run(`UPDATE files SET ctime = datetime("${src.mtime.toISOString()}") WHERE file_id = $id`, {$id: id}) + let {ctime:t1, id, generation, mtime,...src} = await vfs.getDoc(scene_id); await vfs.writeDoc('{"id":3}', "foo", user.uid); @@ -83,9 +75,10 @@ describe("COPY /scenes/:name", function(){ .set("Destination", "http://localhost:8000/scenes/foo/scene.svx.json") .set("Label", generation) .expect(201); - let {ctime:t2, id:id2, generation:g2, ...new_doc} = await expect(vfs.getDoc(scene_id), "file should still be here").to.be.fulfilled; + let {ctime:t2, id:id2, generation:g2, mtime:mt2, ...new_doc} = await expect(vfs.getDoc(scene_id), "file should still be here").to.be.fulfilled; expect(id2).not.to.equal(id); expect(g2).to.equal(4); + expect(mt2).to.be.instanceOf(Date); expect(new_doc).to.deep.equal(src); }); diff --git a/source/server/routes/scenes/copy/document.ts b/source/server/routes/scenes/scene/files/copy/document.ts similarity index 88% rename from source/server/routes/scenes/copy/document.ts rename to source/server/routes/scenes/scene/files/copy/document.ts index 98f46995..6963be4d 100644 --- a/source/server/routes/scenes/copy/document.ts +++ b/source/server/routes/scenes/scene/files/copy/document.ts @@ -1,7 +1,7 @@ import { Request, Response } from "express"; import path from "path"; -import { BadRequestError, ConflictError, ForbiddenError, NotFoundError } from "../../../utils/errors.js"; -import { getFileParams, getUserId, getVfs } from "../../../utils/locals.js"; +import { BadRequestError, ForbiddenError } from "../../../../../utils/errors.js"; +import { getVfs, getUserId } from "../../../../../utils/locals.js"; diff --git a/source/server/routes/scenes/copy/file.ts b/source/server/routes/scenes/scene/files/copy/file.ts similarity index 90% rename from source/server/routes/scenes/copy/file.ts rename to source/server/routes/scenes/scene/files/copy/file.ts index 15690ae2..6ffb85a3 100644 --- a/source/server/routes/scenes/copy/file.ts +++ b/source/server/routes/scenes/scene/files/copy/file.ts @@ -1,8 +1,8 @@ import { Request, Response } from "express"; import path from "path"; -import { BadRequestError, ConflictError, ForbiddenError, NotFoundError } from "../../../utils/errors.js"; -import { getFileParams, getUserId, getVfs } from "../../../utils/locals.js"; +import { BadRequestError, NotFoundError, ForbiddenError } from "../../../../../utils/errors.js"; +import { getVfs, getUserId, getFileParams } from "../../../../../utils/locals.js"; /** diff --git a/source/server/routes/scenes/delete/file.ts b/source/server/routes/scenes/scene/files/delete/file.ts similarity index 83% rename from source/server/routes/scenes/delete/file.ts rename to source/server/routes/scenes/scene/files/delete/file.ts index 05c01cdc..c3fc338a 100644 --- a/source/server/routes/scenes/delete/file.ts +++ b/source/server/routes/scenes/scene/files/delete/file.ts @@ -1,6 +1,5 @@ - -import { getFileParams, getUserId, getVfs } from "../../../utils/locals.js"; import { Request, Response } from "express"; +import { getVfs, getUserId, getFileParams } from "../../../../../utils/locals.js"; /** * @todo use file compression for text assets. Data _should_ be compressed at rest on the server diff --git a/source/server/routes/scenes/get/document.test.ts b/source/server/routes/scenes/scene/files/get/document.test.ts similarity index 78% rename from source/server/routes/scenes/get/document.test.ts rename to source/server/routes/scenes/scene/files/get/document.test.ts index db16d17b..9167ad6c 100644 --- a/source/server/routes/scenes/get/document.test.ts +++ b/source/server/routes/scenes/scene/files/get/document.test.ts @@ -4,13 +4,13 @@ import { randomBytes } from "crypto"; import { fileURLToPath } from 'url'; import request from "supertest"; -import User from "../../../auth/User.js"; -import UserManager from "../../../auth/UserManager.js"; -import { NotFoundError } from "../../../utils/errors.js"; -import Vfs from "../../../vfs/index.js"; +import User from "../../../../../auth/User.js"; +import UserManager from "../../../../../auth/UserManager.js"; +import Vfs from "../../../../../vfs/index.js"; + +import { fixturesDir } from "../../../../../__test_fixtures/fixtures.js"; -const thisDir = path.dirname(fileURLToPath(import.meta.url)); describe("GET /scenes/:scene/scene.svx.json", function(){ @@ -25,7 +25,7 @@ describe("GET /scenes/:scene/scene.svx.json", function(){ user = await userManager.addUser("bob", "12345678"); admin = await userManager.addUser("alice", "12345678", true); - sampleDocString = await fs.readFile(path.resolve(thisDir, "../../../", "__test_fixtures/documents/01_simple.svx.json"), {encoding:"utf8"}); + sampleDocString = await fs.readFile(path.resolve(fixturesDir, "documents/01_simple.svx.json"), {encoding:"utf8"}); }); this.afterAll(async function(){ diff --git a/source/server/routes/scenes/get/document.ts b/source/server/routes/scenes/scene/files/get/document.ts similarity index 94% rename from source/server/routes/scenes/get/document.ts rename to source/server/routes/scenes/scene/files/get/document.ts index 0088525d..65c865e5 100644 --- a/source/server/routes/scenes/get/document.ts +++ b/source/server/routes/scenes/scene/files/get/document.ts @@ -1,8 +1,8 @@ - -import { getVfs } from "../../../utils/locals.js"; import { Request, Response } from "express"; import { createHash } from "crypto"; +import { getVfs } from "../../../../../utils/locals.js"; + /** * @todo use file compression for text assets. Data _should_ be compressed at rest on the server diff --git a/source/server/routes/scenes/get/file.test.ts b/source/server/routes/scenes/scene/files/get/file.test.ts similarity index 95% rename from source/server/routes/scenes/get/file.test.ts rename to source/server/routes/scenes/scene/files/get/file.test.ts index dc94f321..702e190d 100644 --- a/source/server/routes/scenes/get/file.test.ts +++ b/source/server/routes/scenes/scene/files/get/file.test.ts @@ -4,9 +4,10 @@ import {Readable} from "stream"; import timers from "node:timers/promises"; import request from "supertest"; -import User from "../../../auth/User.js"; -import UserManager from "../../../auth/UserManager.js"; -import Vfs from "../../../vfs/index.js"; +import UserManager from "../../../../../auth/UserManager.js"; +import User from "../../../../../auth/User.js"; +import Vfs from "../../../../../vfs/index.js"; + @@ -51,7 +52,7 @@ describe("GET /scenes/:scene/:filename(.*)", function(){ await vfs.writeDoc("{}", scene_id, user.uid); await vfs.writeFile(dataStream(), {scene: "foo", mime:"model/gltf-binary", name: "models/foo.glb", user_id: user.uid}); let agent = request.agent(this.server); - await agent.post("/api/v1/login") + await agent.post("/auth/login") .send({username: user.username, password: "12345678"}) .set("Content-Type", "application/json") .set("Accept", "") diff --git a/source/server/routes/scenes/get/file.ts b/source/server/routes/scenes/scene/files/get/file.ts similarity index 91% rename from source/server/routes/scenes/get/file.ts rename to source/server/routes/scenes/scene/files/get/file.ts index 047a9ce2..ad67a87e 100644 --- a/source/server/routes/scenes/get/file.ts +++ b/source/server/routes/scenes/scene/files/get/file.ts @@ -1,7 +1,7 @@ import {pipeline} from "node:stream/promises"; -import { getFileParams, getUserId, getVfs } from "../../../utils/locals.js"; import { Request, Response } from "express"; +import { getVfs, getFileParams } from "../../../../../utils/locals.js"; /** diff --git a/source/server/routes/scenes/mkcol/folder.test.ts b/source/server/routes/scenes/scene/files/mkcol/folder.test.ts similarity index 93% rename from source/server/routes/scenes/mkcol/folder.test.ts rename to source/server/routes/scenes/scene/files/mkcol/folder.test.ts index a36c6f71..4817744e 100644 --- a/source/server/routes/scenes/mkcol/folder.test.ts +++ b/source/server/routes/scenes/scene/files/mkcol/folder.test.ts @@ -2,9 +2,10 @@ import { randomBytes } from "crypto"; import request from "supertest"; import { expect } from "chai"; -import User from "../../../auth/User.js"; -import UserManager from "../../../auth/UserManager.js"; -import Vfs from "../../../vfs/index.js"; +import User from "../../../../../auth/User.js"; +import UserManager from "../../../../../auth/UserManager.js"; +import Vfs from "../../../../../vfs/index.js"; + diff --git a/source/server/routes/scenes/mkcol/folder.ts b/source/server/routes/scenes/scene/files/mkcol/folder.ts similarity index 77% rename from source/server/routes/scenes/mkcol/folder.ts rename to source/server/routes/scenes/scene/files/mkcol/folder.ts index 2d2eda44..d23daadc 100644 --- a/source/server/routes/scenes/mkcol/folder.ts +++ b/source/server/routes/scenes/scene/files/mkcol/folder.ts @@ -1,7 +1,9 @@ import { Request, Response } from "express"; -import { getFileParams, getUser, getVfs } from "../../../utils/locals.js"; import { normalize } from "path"; -import { BadRequestError } from "../../../utils/errors.js"; + +import { BadRequestError } from "../../../../../utils/errors.js"; +import { getVfs, getUser, getFileParams } from "../../../../../utils/locals.js"; + diff --git a/source/server/routes/scenes/move/file.ts b/source/server/routes/scenes/scene/files/move/file.ts similarity index 86% rename from source/server/routes/scenes/move/file.ts rename to source/server/routes/scenes/scene/files/move/file.ts index b8698d6a..618e31f1 100644 --- a/source/server/routes/scenes/move/file.ts +++ b/source/server/routes/scenes/scene/files/move/file.ts @@ -1,7 +1,8 @@ import { Request, Response } from "express"; -import path from "path"; -import { BadRequestError, ForbiddenError } from "../../../utils/errors.js"; -import { getFileParams, getUserId, getVfs } from "../../../utils/locals.js"; + +import { BadRequestError, ForbiddenError } from "../../../../../utils/errors.js"; +import { getVfs, getUserId, getFileParams } from "../../../../../utils/locals.js"; + /** diff --git a/source/server/routes/scenes/move/move.test.ts b/source/server/routes/scenes/scene/files/move/move.test.ts similarity index 86% rename from source/server/routes/scenes/move/move.test.ts rename to source/server/routes/scenes/scene/files/move/move.test.ts index fc11c9bb..efedc436 100644 --- a/source/server/routes/scenes/move/move.test.ts +++ b/source/server/routes/scenes/scene/files/move/move.test.ts @@ -1,15 +1,11 @@ -import fs from "fs/promises"; -import path from "path"; -import {tmpdir} from "os"; - import request from "supertest"; import { expect } from "chai"; -import express, { Application } from "express"; -import User from "../../../auth/User.js"; -import UserManager from "../../../auth/UserManager.js"; -import Vfs, { WriteFileParams } from "../../../vfs/index.js"; -import { Element, xml2js } from "xml-js"; +import User from "../../../../../auth/User.js"; +import UserManager from "../../../../../auth/UserManager.js"; +import Vfs from "../../../../../vfs/index.js"; +import { WriteFileParams } from "../../../../../vfs/types.js"; + @@ -28,7 +24,7 @@ describe("MOVE /scenes/:name", function(){ this.agent = request.agent(this.server); - await this.agent.post("/api/v1/login") + await this.agent.post("/auth/login") .send({username: user.username, password: "12345678"}) .set("Content-Type", "application/json") .set("Accept", "") diff --git a/source/server/routes/scenes/put/document.test.ts b/source/server/routes/scenes/scene/files/put/document.test.ts similarity index 91% rename from source/server/routes/scenes/put/document.test.ts rename to source/server/routes/scenes/scene/files/put/document.test.ts index 8783a216..5c375062 100644 --- a/source/server/routes/scenes/put/document.test.ts +++ b/source/server/routes/scenes/scene/files/put/document.test.ts @@ -1,17 +1,15 @@ import fs from "fs/promises"; import path from "path"; import { randomBytes } from "crypto"; -import { fileURLToPath } from 'url'; import request from "supertest"; -import User from "../../../auth/User.js"; -import UserManager from "../../../auth/UserManager.js"; -import { NotFoundError } from "../../../utils/errors.js"; -import Vfs from "../../../vfs/index.js"; -import uid from "../../../utils/uid.js"; +import User from "../../../../../auth/User.js"; +import UserManager from "../../../../../auth/UserManager.js"; +import uid from "../../../../../utils/uid.js"; +import Vfs from "../../../../../vfs/index.js"; -const thisDir = path.dirname(fileURLToPath(import.meta.url)); +import { fixturesDir } from "../../../../../__test_fixtures/fixtures.js"; describe("PUT /scenes/:scene/scene.svx.json", function(){ @@ -26,7 +24,7 @@ describe("PUT /scenes/:scene/scene.svx.json", function(){ user = await userManager.addUser("bob", "12345678"); admin = await userManager.addUser("alice", "12345678", true); - sampleDocString = await fs.readFile(path.resolve(thisDir, "../../../", "__test_fixtures/documents/01_simple.svx.json"), {encoding:"utf8"}); + sampleDocString = await fs.readFile(path.resolve(fixturesDir,"documents/01_simple.svx.json"), {encoding:"utf8"}); }); this.afterAll(async function(){ await cleanIntegrationContext(this); diff --git a/source/server/routes/scenes/put/document.ts b/source/server/routes/scenes/scene/files/put/document.ts similarity index 89% rename from source/server/routes/scenes/put/document.ts rename to source/server/routes/scenes/scene/files/put/document.ts index ae70ca09..330ebd89 100644 --- a/source/server/routes/scenes/put/document.ts +++ b/source/server/routes/scenes/scene/files/put/document.ts @@ -2,10 +2,10 @@ import {inspect} from "util"; import path from "path"; import { Request, Response } from "express"; -import { getLocals, getUserId, getVfs } from "../../../utils/locals.js"; -import { BadRequestError } from "../../../utils/errors.js"; +import * as merge from "../../../../../utils/merge/index.js"; +import { BadRequestError } from "../../../../../utils/errors.js"; +import { getLocals, getUserId, getVfs } from "../../../../../utils/locals.js"; -import * as merge from "../../../utils/merge/index.js"; /** * Special handler for svx files to disallow the upload of invalid JSON. diff --git a/source/server/routes/scenes/put/file.test.ts b/source/server/routes/scenes/scene/files/put/file.test.ts similarity index 90% rename from source/server/routes/scenes/put/file.test.ts rename to source/server/routes/scenes/scene/files/put/file.test.ts index 26dfd862..fa81864d 100644 --- a/source/server/routes/scenes/put/file.test.ts +++ b/source/server/routes/scenes/scene/files/put/file.test.ts @@ -1,9 +1,10 @@ import request from "supertest"; -import User from "../../../auth/User.js"; -import UserManager from "../../../auth/UserManager.js"; -import { NotFoundError } from "../../../utils/errors.js"; -import Vfs from "../../../vfs/index.js"; + +import User from "../../../../../auth/User.js"; +import UserManager from "../../../../../auth/UserManager.js"; +import { NotFoundError } from "../../../../../utils/errors.js"; +import Vfs from "../../../../../vfs/index.js"; diff --git a/source/server/routes/scenes/put/file.ts b/source/server/routes/scenes/scene/files/put/file.ts similarity index 79% rename from source/server/routes/scenes/put/file.ts rename to source/server/routes/scenes/scene/files/put/file.ts index 92ac2001..5d799882 100644 --- a/source/server/routes/scenes/put/file.ts +++ b/source/server/routes/scenes/scene/files/put/file.ts @@ -1,7 +1,8 @@ - -import { getFileParams, getUserId, getVfs } from "../../../utils/locals.js"; import { Request, Response } from "express"; -import { getContentType, getMimeType } from "../../../utils/filetypes.js"; + +import { getMimeType, getContentType } from "../../../../../utils/filetypes.js"; +import { getVfs, getUserId, getFileParams } from "../../../../../utils/locals.js"; + export default async function handlePutFile(req :Request, res :Response){ diff --git a/source/server/routes/scenes/put/index.ts b/source/server/routes/scenes/scene/files/put/index.ts similarity index 100% rename from source/server/routes/scenes/put/index.ts rename to source/server/routes/scenes/scene/files/put/index.ts diff --git a/source/server/routes/api/v1/scenes/scene/get.test.ts b/source/server/routes/scenes/scene/get.test.ts similarity index 80% rename from source/server/routes/api/v1/scenes/scene/get.test.ts rename to source/server/routes/scenes/scene/get.test.ts index f0539289..4155e62d 100644 --- a/source/server/routes/api/v1/scenes/scene/get.test.ts +++ b/source/server/routes/scenes/scene/get.test.ts @@ -1,19 +1,13 @@ -import fs, { FileHandle } from "fs/promises"; -import path from "path"; -import {tmpdir} from "os"; -import {Readable} from "stream"; - import request from "supertest"; import { expect } from "chai"; -import Vfs from "../../../../../vfs/index.js"; -import UserManager from "../../../../../auth/UserManager.js"; -import User from "../../../../../auth/User.js"; -import { read_cdh } from "../../../../../utils/zip/index.js"; +import UserManager from "../../../auth/UserManager.js"; +import { read_cdh } from "../../../utils/zip/index.js"; +import { HandleMock } from "../../../utils/zip/zip.test.js"; +import Vfs from "../../../vfs/index.js"; -import { HandleMock } from "../../../../../utils/zip/zip.test.js"; -describe("GET /api/v1/scenes/:scene", function(){ +describe("GET /scenes/:scene", function(){ let vfs:Vfs, userManager:UserManager, ids :number[]; this.beforeEach(async function(){ let locals = await createIntegrationContext(this); @@ -31,14 +25,14 @@ describe("GET /api/v1/scenes/:scene", function(){ }); describe("as application/json", function(){ it("get scene info", async function(){ - await request(this.server).get("/api/v1/scenes/foo") + await request(this.server).get("/scenes/foo") .expect(200) .expect("Content-Type", "application/json; charset=utf-8"); }); it("is access-protected (obfuscated as 404)", async function(){ await userManager.grant("foo", "default", "none"); - await request(this.server).get("/api/v1/scenes/foo") + await request(this.server).get("/scenes/foo") .expect(404); }); }); @@ -49,7 +43,7 @@ describe("GET /api/v1/scenes/:scene", function(){ await vfs._db.run(`UPDATE files SET ctime = datetime("${t.toISOString()}")`); await vfs._db.run(`UPDATE documents SET ctime = datetime("${t.toISOString()}")`); - let res = await request(this.server).get("/api/v1/scenes/foo") + let res = await request(this.server).get("/scenes/foo") .set("Accept", "application/zip") .expect(200) .expect("Content-Type", "application/zip"); @@ -114,7 +108,7 @@ describe("GET /api/v1/scenes/:scene", function(){ }); it("can use query params to set request format", async function(){ - await request(this.server).get("/api/v1/scenes/foo?format=zip") + await request(this.server).get("/scenes/foo?format=zip") .expect(200) .expect("Content-Type", "application/zip"); }); diff --git a/source/server/routes/api/v1/scenes/scene/get.ts b/source/server/routes/scenes/scene/get.ts similarity index 87% rename from source/server/routes/api/v1/scenes/scene/get.ts rename to source/server/routes/scenes/scene/get.ts index 3b6639e7..694ca613 100644 --- a/source/server/routes/api/v1/scenes/scene/get.ts +++ b/source/server/routes/scenes/scene/get.ts @@ -1,12 +1,13 @@ import { Request, Response } from "express"; import path from "path"; -import { getUserId, getVfs } from "../../../../../utils/locals.js"; -import { wrapFormat } from "../../../../../utils/wrapAsync.js"; -import { ZipEntry, zip } from "../../../../../utils/zip/index.js"; -import { HTTPError } from "../../../../../utils/errors.js"; import { once } from "events"; +import { HTTPError } from "../../../utils/errors.js"; +import { getVfs, getUserId } from "../../../utils/locals.js"; +import { wrapFormat } from "../../../utils/wrapAsync.js"; +import { ZipEntry, zip } from "../../../utils/zip/index.js"; + diff --git a/source/server/routes/scenes/mkcol/scene.test.ts b/source/server/routes/scenes/scene/mkcol.test.ts similarity index 100% rename from source/server/routes/scenes/mkcol/scene.test.ts rename to source/server/routes/scenes/scene/mkcol.test.ts diff --git a/source/server/routes/scenes/mkcol/scene.ts b/source/server/routes/scenes/scene/mkcol.ts similarity index 100% rename from source/server/routes/scenes/mkcol/scene.ts rename to source/server/routes/scenes/scene/mkcol.ts diff --git a/source/server/routes/api/v1/scenes/scene/patch.test.ts b/source/server/routes/scenes/scene/patch.test.ts similarity index 59% rename from source/server/routes/api/v1/scenes/scene/patch.test.ts rename to source/server/routes/scenes/scene/patch.test.ts index 841b5dd8..f98d3e94 100644 --- a/source/server/routes/api/v1/scenes/scene/patch.test.ts +++ b/source/server/routes/scenes/scene/patch.test.ts @@ -1,11 +1,12 @@ - - import request from "supertest"; -import Vfs from "../../../../../vfs/index.js"; -import UserManager from "../../../../../auth/UserManager.js"; +import { randomBytes } from "crypto"; -describe("PATCH /api/v1/scenes/:scene", function(){ +import User from "../../../auth/User.js"; +import Vfs from "../../../vfs/index.js"; +import UserManager from "../../../auth/UserManager.js"; + +describe("PATCH /scenes/:scene", function(){ let vfs:Vfs, userManager:UserManager, ids :number[]; this.beforeEach(async function(){ let locals = await createIntegrationContext(this); @@ -21,15 +22,17 @@ describe("PATCH /api/v1/scenes/:scene", function(){ await cleanIntegrationContext(this); }); - describe("rename a scene", function(){ - it("get scene info", async function(){ - await request(this.server).patch("/api/v1/scenes/foo") + describe("rename", function(){ + it("change name", async function(){ + await request(this.server).patch("/scenes/foo") + .set("Content-Type", "application/json") .send({name: "foofoo"}) .expect(200); }); it("forces unique names", async function(){ - await request(this.server).patch("/api/v1/scenes/foo") + await request(this.server).patch("/scenes/foo") + .set("Content-Type", "application/json") .send({name: "bar"}) .expect(409); }) @@ -37,17 +40,18 @@ describe("PATCH /api/v1/scenes/:scene", function(){ it("is admin-protected", async function(){ await userManager.grant("foo", "any", "write"); await userManager.grant("foo", "default", "write"); - await request(this.server).patch("/api/v1/scenes/foo") + await request(this.server).patch("/scenes/foo") + .set("Content-Type", "application/json") .send({name: "foofoo"}) .expect(401); }); it("is access-protected (obfuscated as 404)", async function(){ await userManager.grant("foo", "default", "none"); - await request(this.server).patch("/api/v1/scenes/foo") + await request(this.server).patch("/scenes/foo") + .set("Content-Type", "application/json") .send({name: "foofoo"}) .expect(404); }); }); - }); diff --git a/source/server/routes/api/v1/scenes/scene/patch.ts b/source/server/routes/scenes/scene/patch.ts similarity index 69% rename from source/server/routes/api/v1/scenes/scene/patch.ts rename to source/server/routes/scenes/scene/patch.ts index 1d9b424d..8c2633f4 100644 --- a/source/server/routes/api/v1/scenes/scene/patch.ts +++ b/source/server/routes/scenes/scene/patch.ts @@ -1,6 +1,6 @@ -import { ConflictError } from "../../../../../utils/errors.js"; -import { getUserId, getVfs } from "../../../../../utils/locals.js"; +import { ConflictError } from "../../../utils/errors.js"; +import { getUserId, getUserManager, getVfs } from "../../../utils/locals.js"; import { Request, Response } from "express"; @@ -15,7 +15,7 @@ export default async function patchScene(req :Request, res :Response){ let vfs = getVfs(req); let user_id = getUserId(req); let {scene} = req.params; - let {name} = req.body; + let {name, permissions} = req.body; //Ensure all or none of the changes are comitted let result = await getVfs(req).isolate(async (vfs)=>{ let {id} = await vfs.getScene(scene, user_id); @@ -31,6 +31,16 @@ export default async function patchScene(req :Request, res :Response){ } } + if(permissions){ + let userManager = getUserManager(req); + let {scene} = req.params; + for(let key in permissions){ + await userManager.grant(scene, key, permissions[key]); + } + } + + + return await vfs.getScene(id, user_id); }); res.status(200).send(result); diff --git a/source/server/routes/api/v1/scenes/scene/post.test.ts b/source/server/routes/scenes/scene/post.test.ts similarity index 84% rename from source/server/routes/api/v1/scenes/scene/post.test.ts rename to source/server/routes/scenes/scene/post.test.ts index 64a41ffb..d70a4690 100644 --- a/source/server/routes/api/v1/scenes/scene/post.test.ts +++ b/source/server/routes/scenes/scene/post.test.ts @@ -6,18 +6,19 @@ import timers from 'timers/promises'; import request from "supertest"; import { expect } from "chai"; -import { once } from "events"; + import postScene from "./post.js"; import express, { Application } from "express"; -import wrap from "../../../../../utils/wrapAsync.js"; -import Vfs from "../../../../../vfs/index.js"; +import wrap from "../../../utils/wrapAsync.js"; +import Vfs from "../../../vfs/index.js"; + +import { fixturesDir } from "../../../__test_fixtures/fixtures.js"; -const thisDir = path.dirname(fileURLToPath(import.meta.url)); -describe("POST /api/v1/scenes/:scene", function(){ +describe("POST /scenes/:scene", function(){ let vfs:Vfs, app: Application, data:Buffer; this.beforeEach(async function(){ - data = await fs.readFile(path.join(thisDir, "../../../../../__test_fixtures/cube.glb")); + data = await fs.readFile(path.join(fixturesDir, "cube.glb")); this.dir = await fs.mkdtemp(path.join(tmpdir(), `scenes-integration`)); vfs = await Vfs.Open(this.dir,{createDirs: true}); app = express(); diff --git a/source/server/routes/api/v1/scenes/scene/post.ts b/source/server/routes/scenes/scene/post.ts similarity index 88% rename from source/server/routes/api/v1/scenes/scene/post.ts rename to source/server/routes/scenes/scene/post.ts index 653b214e..50e4be08 100644 --- a/source/server/routes/api/v1/scenes/scene/post.ts +++ b/source/server/routes/scenes/scene/post.ts @@ -1,8 +1,8 @@ - -import { getUserId, getVfs } from "../../../../../utils/locals.js"; import { Request, Response } from "express"; -import { parse_glb } from "../../../../../utils/glTF.js"; -import uid from "../../../../../utils/uid.js"; + +import { parse_glb } from "../../../utils/glTF.js"; +import { getVfs, getUserId } from "../../../utils/locals.js"; +import uid from "../../../utils/uid.js"; /** @@ -13,7 +13,7 @@ import uid from "../../../../../utils/uid.js"; * @returns */ async function getDocument(scene:string, filepath:string){ - let {default:orig} = await import("../../../../../utils/schema/default.svx.json", {assert:{type:"json"}}); + let {default:orig} = await import("../../../utils/schema/default.svx.json", {assert:{type:"json"}}); //dumb inefficient Deep copy because we want to mutate the doc in-place let document = JSON.parse(JSON.stringify(orig)); let meta = await parse_glb(filepath); diff --git a/source/server/routes/users/index.ts b/source/server/routes/users/index.ts new file mode 100644 index 00000000..76159888 --- /dev/null +++ b/source/server/routes/users/index.ts @@ -0,0 +1,38 @@ + +import { Router } from "express"; + +import UserManager from "../../auth/UserManager.js"; +import { getUserManager, isAdministrator, isAdministratorOrOpen, isUser } from "../../utils/locals.js"; +import wrap from "../../utils/wrapAsync.js"; +import bodyParser from "body-parser"; + +import postUser from "./post.js"; +import handleDeleteUser from "./uid/delete.js"; +import { handlePatchUser } from "./uid/patch.js"; + +const router = Router(); + +/** Configure cache behaviour for the whole API + * Settings can be changed individually further down the line + */ +router.use((req, res, next)=>{ + //Browser should always make the request + res.set("Cache-Control", "no-cache"); + next(); +}); + + + +router.get("/", isAdministrator, wrap(async (req, res)=>{ + let userManager :UserManager = getUserManager(req); + //istanbul ignore if + if(!userManager) throw new Error("Badly configured app : userManager is not defined in app.locals"); + let users = await userManager.getUsers(true); + res.status(200).send(users); +})); + +router.post("/", isAdministratorOrOpen, bodyParser.json(), wrap(postUser)); +router.delete("/:uid", isAdministrator, wrap(handleDeleteUser)); +router.patch("/:uid", bodyParser.json(), wrap(handlePatchUser)); + +export default router; diff --git a/source/server/routes/api/v1/users/post.ts b/source/server/routes/users/post.ts similarity index 77% rename from source/server/routes/api/v1/users/post.ts rename to source/server/routes/users/post.ts index 5f5b7f64..3698cacd 100644 --- a/source/server/routes/api/v1/users/post.ts +++ b/source/server/routes/users/post.ts @@ -1,9 +1,10 @@ import { Request, Response } from "express"; -import User from "../../../../auth/User.js"; -import UserManager from "../../../../auth/UserManager.js"; -import { BadRequestError } from "../../../../utils/errors.js"; -import { getUserManager } from "../../../../utils/locals.js"; + +import { getUserManager } from "../../utils/locals.js"; +import User from "../../auth/User.js"; +import UserManager from "../../auth/UserManager.js"; +import { BadRequestError } from "../../utils/errors.js"; diff --git a/source/server/routes/api/v1/users/uid/delete.ts b/source/server/routes/users/uid/delete.ts similarity index 62% rename from source/server/routes/api/v1/users/uid/delete.ts rename to source/server/routes/users/uid/delete.ts index 1b2ab145..4fa1ee45 100644 --- a/source/server/routes/api/v1/users/uid/delete.ts +++ b/source/server/routes/users/uid/delete.ts @@ -1,10 +1,9 @@ import { Request, Response } from "express"; -import User from "../../../../../auth/User.js"; -import UserManager from "../../../../../auth/UserManager.js"; -import { BadRequestError } from "../../../../../utils/errors.js"; -import { getUserId, getUserManager } from "../../../../../utils/locals.js"; +import { getUserId, getUserManager } from "../../../utils/locals.js"; +import { BadRequestError } from "../../../utils/errors.js"; +import UserManager from "../../../auth/UserManager.js"; diff --git a/source/server/routes/api/v1/users/uid/patch.ts b/source/server/routes/users/uid/patch.ts similarity index 81% rename from source/server/routes/api/v1/users/uid/patch.ts rename to source/server/routes/users/uid/patch.ts index 9ccd18c8..a2a47cb0 100644 --- a/source/server/routes/api/v1/users/uid/patch.ts +++ b/source/server/routes/users/uid/patch.ts @@ -1,7 +1,8 @@ import { Request, Response } from "express"; -import User, { SafeUser } from "../../../../../auth/User.js"; -import { UnauthorizedError } from "../../../../../utils/errors.js"; -import { getUser, getUserManager } from "../../../../../utils/locals.js"; + +import { getUser, getUserManager } from "../../../utils/locals.js"; +import User, { SafeUser } from "../../../auth/User.js"; +import { UnauthorizedError } from "../../../utils/errors.js"; diff --git a/source/server/routes/api/v1/users/users.test.ts b/source/server/routes/users/users.test.ts similarity index 81% rename from source/server/routes/api/v1/users/users.test.ts rename to source/server/routes/users/users.test.ts index 8511e86b..838b2485 100644 --- a/source/server/routes/api/v1/users/users.test.ts +++ b/source/server/routes/users/users.test.ts @@ -1,13 +1,15 @@ import request from "supertest"; -import Vfs, { WriteFileParams } from "../../../../vfs/index.js"; -import User from "../../../../auth/User.js"; -import UserManager from "../../../../auth/UserManager.js"; -import { NotFoundError } from "../../../../utils/errors.js"; +import User from "../../auth/User.js"; +import UserManager from "../../auth/UserManager.js"; +import { NotFoundError } from "../../utils/errors.js"; +import Vfs from "../../vfs/index.js"; +import { WriteFileParams } from "../../vfs/types.js"; -describe("/api/v1/users", function(){ + +describe("/users", function(){ let vfs :Vfs, userManager :UserManager; this.beforeEach(async function(){ let locals = await createIntegrationContext(this); @@ -20,7 +22,7 @@ describe("/api/v1/users", function(){ describe("in open mode", function(){ it("POST can create a user anonymously", async function(){ - await request(this.server).post("/api/v1/users") + await request(this.server).post("/users") .send({username:"foo", password:"12345678", email:"foo@example.com", isAdministrator: true}) .expect(201); }); @@ -36,7 +38,7 @@ describe("/api/v1/users", function(){ describe("as user", function(){ this.beforeEach(async function(){ this.agent = request.agent(this.server); - await this.agent.post("/api/v1/login") + await this.agent.post("/auth/login") .send({username: user.username, password: "12345678"}) .set("Content-Type", "application/json") .set("Accept", "") @@ -44,18 +46,18 @@ describe("/api/v1/users", function(){ }) it("can't create a user", async function(){ - await this.agent.post("/api/v1/users") + await this.agent.post("/users") .send({username:"foo", password:"12345678", email:"foo@example.com", isAdministrator: true}) .expect(401); }); it("can't remove a user", async function(){ - await this.agent.delete(`/api/v1/users/${user.uid}`) + await this.agent.delete(`/users/${user.uid}`) .expect(401); }); it("can patch another user", async function(){ - await this.agent.patch(`/api/v1/users/${admin.uid}`) + await this.agent.patch(`/users/${admin.uid}`) .send({username:"foo"}) .expect(401); @@ -64,7 +66,7 @@ describe("/api/v1/users", function(){ }); it("can patch himself", async function(){ - await this.agent.patch(`/api/v1/users/${user.uid}`) + await this.agent.patch(`/users/${user.uid}`) .send({username:"foo"}) .expect(200); expect(await userManager.getUserByName("foo")).to.be.ok; @@ -72,7 +74,7 @@ describe("/api/v1/users", function(){ }); it("can't patch himself administrator", async function(){ - await this.agent.patch(`/api/v1/users/${user.uid}`) + await this.agent.patch(`/users/${user.uid}`) .send({isAdministrator: true}) .expect(401); @@ -80,7 +82,7 @@ describe("/api/v1/users", function(){ }); it("can't fetch user list", async function(){ - await this.agent.get("/api/v1/users") + await this.agent.get("/users") .expect(401); }); }); @@ -88,14 +90,14 @@ describe("/api/v1/users", function(){ describe("as administrator", function(){ this.beforeEach(async function(){ this.agent = request.agent(this.server); - await this.agent.post("/api/v1/login") + await this.agent.post("/auth/login") .send({username: admin.username, password: "12345678"}) .set("Content-Type", "application/json") .set("Accept", "") .expect(200); }) it("can get a list of users", async function(){ - let res = await this.agent.get("/api/v1/users") + let res = await this.agent.get("/users") .set("Accept", "application/json") .expect(200); expect(res.body).to.have.property("length", 2); @@ -107,21 +109,21 @@ describe("/api/v1/users", function(){ }) it("can create a user", async function(){ - await this.agent.post("/api/v1/users") + await this.agent.post("/users") .set("Content-Type", "application/json") .send({username: "Carol", password: "abcdefghij", isAdministrator: false, email: "carol@foo.com"}) .expect(201); }); it("can create an admin", async function(){ - await this.agent.post("/api/v1/users") + await this.agent.post("/users") .set("Content-Type", "application/json") .send({username: "Dave", password: "abcdefghij", isAdministrator: true, email: "dave@foo.com"}) .expect(201); }); it("can't provide bad data'", async function(){ - await this.agent.post("/api/v1/users") + await this.agent.post("/users") .set("Content-Type", "application/json") .send({username: "Oscar", password: "abcdefghij", isAdministrator: "foo"}) .expect(400); @@ -129,14 +131,14 @@ describe("/api/v1/users", function(){ it("can remove a user", async function(){ let users = await (await userManager.getUsers()).length; - await this.agent.delete(`/api/v1/users/${user.uid}`) + await this.agent.delete(`/users/${user.uid}`) .expect(204); expect(await userManager.getUsers()).to.have.property("length",users - 1); }); it("can't remove himself", async function(){ let users = await (await userManager.getUsers()).length; - await this.agent.delete(`/api/v1/users/${admin.uid}`) + await this.agent.delete(`/users/${admin.uid}`) .expect(400); expect(await userManager.getUsers()).to.have.property("length",users); }); @@ -151,7 +153,7 @@ describe("/api/v1/users", function(){ {uid: user.uid, username: user.username, access: "admin"}, ]); let f = await vfs.createFile(props, {hash: "xxxxxx", size:10}); - await this.agent.delete(`/api/v1/users/${user.uid}`) + await this.agent.delete(`/users/${user.uid}`) .expect(204); expect(await vfs.getFileProps(props)).to.have.property("author", "default"); expect(await userManager.getPermissions(scene)).to.deep.equal([ @@ -161,7 +163,7 @@ describe("/api/v1/users", function(){ }); it("can patch a user", async function(){ - await this.agent.patch(`/api/v1/users/${user.uid}`) + await this.agent.patch(`/users/${user.uid}`) .send({username:"foo"}) .expect(200); @@ -170,7 +172,7 @@ describe("/api/v1/users", function(){ }); it("can patch a user as administrator", async function(){ - await this.agent.patch(`/api/v1/users/${user.uid}`) + await this.agent.patch(`/users/${user.uid}`) .send({isAdministrator: true}) .expect(200); @@ -178,7 +180,7 @@ describe("/api/v1/users", function(){ }); it("can't patch himself as non-admin", async function(){ - await this.agent.patch(`/api/v1/users/${admin.uid}`) + await this.agent.patch(`/users/${admin.uid}`) .send({isAdministrator: false}) .expect(401); }); diff --git a/source/server/tests-common.ts b/source/server/tests-common.ts index 6b7eed00..7cdad67a 100644 --- a/source/server/tests-common.ts +++ b/source/server/tests-common.ts @@ -31,7 +31,7 @@ global.dataStream = async function* (src :Array =["foo", "\n"]){ } global.createIntegrationContext = async function(c :Mocha.Context, config_override :Partial={}){ - let {default:createServer} = await import("./server.js"); + let {default:createServer} = await import("./routes/index.js"); let titleSlug = c.currentTest?.title.replace(/[^\w]/g, "_") ?? `eThesaurus_integration_test`; c.dir = await fs.mkdtemp(path.join(tmpdir(), titleSlug)); c.config = Object.assign( diff --git a/source/server/utils/glTF.test.ts b/source/server/utils/glTF.test.ts index 18bd64ef..ddef8c89 100644 --- a/source/server/utils/glTF.test.ts +++ b/source/server/utils/glTF.test.ts @@ -5,11 +5,13 @@ import { fileURLToPath } from 'url'; import { parse_glb, parse_glTF } from "./glTF.js"; const thisFile = fileURLToPath(import.meta.url); -const thisDir = path.dirname(thisFile); + +import { fixturesDir } from "../__test_fixtures/fixtures.js"; + describe("parse_glb()", function(){ it("parse a glb file to extract data", async function(){ - let d = await parse_glb(path.resolve(thisDir, "../__test_fixtures/cube.glb" )); + let d = await parse_glb(path.resolve(fixturesDir, "cube.glb" )); expect(d).to.deep.equal({ meshes:[{ numFaces: 6*2 /*it takes two triangles to make a square and 6 squares for a cube */, @@ -35,7 +37,7 @@ describe("parse_glb()", function(){ describe("parse_gltf()", function(){ it("handles morph targets", async function(){ - let data = await fs.readFile(path.resolve(thisDir, "../__test_fixtures/morph.gltf" ), {encoding: "utf8"}); + let data = await fs.readFile(path.resolve(fixturesDir, "morph.gltf" ), {encoding: "utf8"}); let gltf = JSON.parse(data); let res = parse_glTF(gltf); expect(res).to.deep.equal({ diff --git a/source/server/utils/merge/merge.test.ts b/source/server/utils/merge/merge.test.ts index 060878de..0d75af98 100644 --- a/source/server/utils/merge/merge.test.ts +++ b/source/server/utils/merge/merge.test.ts @@ -1,8 +1,7 @@ import fs from "fs/promises"; import path from "path"; -import { fileURLToPath } from 'url'; -const thisDir = path.dirname(fileURLToPath(import.meta.url)); +import { fixturesDir } from "../../__test_fixtures/fixtures.js"; @@ -52,7 +51,7 @@ describe("three-way merge", function(){ describe("merge documents", function(){ let docString :string, doc :IDocument; this.beforeAll(async function(){ - docString = await fs.readFile(path.resolve(thisDir, "../../__test_fixtures/documents/01_simple.svx.json"), "utf8"); + docString = await fs.readFile(path.resolve(fixturesDir, "documents/01_simple.svx.json"), "utf8"); }); this.beforeEach(function(){ doc = JSON.parse(docString); @@ -115,7 +114,7 @@ describe("merge documents", function(){ "03_tours.svx.json", "04_tours.svx.json", ].map( async (file) => { - const str = await fs.readFile(path.resolve(thisDir, "../../__test_fixtures/documents/", file), {encoding:"utf8"}); + const str = await fs.readFile(path.resolve(fixturesDir, "documents/", file), {encoding:"utf8"}); return JSON.parse(str); }));