Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d993c9490 | |||
| dc7f7a5042 | |||
| c0a86753bd | |||
| f277d08982 | |||
| 49d53ccfc2 | |||
| 98e44f528f | |||
| 0e79795856 | |||
| 09b2d0babe |
@@ -24,7 +24,7 @@ const PageTitle = ({ title, favicon }: Props) => {
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
href={cdnPath("/favicon-32.png")}
|
||||
href={cdnPath("/images/favicon-32.png")}
|
||||
sizes="32x32"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -109,7 +109,9 @@ class AddPeopleToCollection extends React.Component<Props> {
|
||||
<Empty>{t("No people left to add")}</Empty>
|
||||
)
|
||||
}
|
||||
items={users.notInCollection(collection.id, this.query)}
|
||||
items={users
|
||||
.notInCollection(collection.id, this.query)
|
||||
.filter((member) => member.id !== user.id)}
|
||||
fetch={this.query ? undefined : users.fetchPage}
|
||||
renderItem={(item: User) => (
|
||||
<MemberListItem
|
||||
|
||||
@@ -123,7 +123,6 @@
|
||||
"koa-router": "7.4.0",
|
||||
"koa-send": "5.0.1",
|
||||
"koa-sslify": "2.1.2",
|
||||
"koa-static": "^4.0.1",
|
||||
"koa-useragent": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mammoth": "^1.4.19",
|
||||
@@ -247,8 +246,8 @@
|
||||
"@types/koa-logger": "^3.1.2",
|
||||
"@types/koa-mount": "^4.0.1",
|
||||
"@types/koa-router": "^7.4.4",
|
||||
"@types/koa-send": "^4.1.3",
|
||||
"@types/koa-sslify": "^2.1.0",
|
||||
"@types/koa-static": "^4.0.2",
|
||||
"@types/koa-useragent": "^2.1.2",
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
"@types/markdown-it-container": "^2.0.4",
|
||||
@@ -348,5 +347,5 @@
|
||||
"js-yaml": "^3.14.1",
|
||||
"jpeg-js": "0.4.4"
|
||||
},
|
||||
"version": "0.65.2"
|
||||
"version": "0.66.3"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 278 B After Width: | Height: | Size: 278 B |
|
Before Width: | Height: | Size: 443 B After Width: | Height: | Size: 443 B |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
@@ -0,0 +1,41 @@
|
||||
import { CollectionPermission } from "@shared/types";
|
||||
import { CollectionUser } from "@server/models";
|
||||
import { UserRole } from "@server/models/User";
|
||||
import { buildUser, buildAdmin, buildCollection } from "@server/test/factories";
|
||||
import { getTestDatabase } from "@server/test/support";
|
||||
import userDemoter from "./userDemoter";
|
||||
|
||||
const db = getTestDatabase();
|
||||
|
||||
afterAll(db.disconnect);
|
||||
|
||||
beforeEach(db.flush);
|
||||
|
||||
describe("userDemoter", () => {
|
||||
const ip = "127.0.0.1";
|
||||
|
||||
it("should change role and associated collection permissions", async () => {
|
||||
const admin = await buildAdmin();
|
||||
const user = await buildUser({ teamId: admin.teamId });
|
||||
const collection = await buildCollection({ teamId: admin.teamId });
|
||||
|
||||
const membership = await CollectionUser.create({
|
||||
createdById: admin.id,
|
||||
userId: user.id,
|
||||
collectionId: collection.id,
|
||||
permission: CollectionPermission.ReadWrite,
|
||||
});
|
||||
|
||||
await userDemoter({
|
||||
user,
|
||||
actorId: admin.id,
|
||||
to: UserRole.Viewer,
|
||||
ip,
|
||||
});
|
||||
|
||||
expect(user.isViewer).toEqual(true);
|
||||
|
||||
await membership.reload();
|
||||
expect(membership.permission).toEqual(CollectionPermission.Read);
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,6 @@ import "./logging/tracing"; // must come before importing any instrumented modul
|
||||
import http from "http";
|
||||
import https from "https";
|
||||
import Koa from "koa";
|
||||
import compress from "koa-compress";
|
||||
import helmet from "koa-helmet";
|
||||
import logger from "koa-logger";
|
||||
import onerror from "koa-onerror";
|
||||
@@ -81,7 +80,6 @@ async function start(id: number, disconnect: () => void) {
|
||||
app.use(logger((str) => Logger.info("http", str)));
|
||||
}
|
||||
|
||||
app.use(compress());
|
||||
app.use(helmet());
|
||||
|
||||
// catch errors in one place, automatically set status and response headers
|
||||
|
||||
@@ -28,6 +28,7 @@ import env from "@server/env";
|
||||
import { ValidationError } from "../errors";
|
||||
import ApiKey from "./ApiKey";
|
||||
import Collection from "./Collection";
|
||||
import CollectionUser from "./CollectionUser";
|
||||
import NotificationSetting from "./NotificationSetting";
|
||||
import Star from "./Star";
|
||||
import Team from "./Team";
|
||||
@@ -419,7 +420,7 @@ class User extends ParanoidModel {
|
||||
|
||||
if (res.count >= 1) {
|
||||
if (to === "member") {
|
||||
return this.update(
|
||||
await this.update(
|
||||
{
|
||||
isAdmin: false,
|
||||
isViewer: false,
|
||||
@@ -427,13 +428,24 @@ class User extends ParanoidModel {
|
||||
options
|
||||
);
|
||||
} else if (to === "viewer") {
|
||||
return this.update(
|
||||
await this.update(
|
||||
{
|
||||
isAdmin: false,
|
||||
isViewer: true,
|
||||
},
|
||||
options
|
||||
);
|
||||
await CollectionUser.update(
|
||||
{
|
||||
permission: CollectionPermission.Read,
|
||||
},
|
||||
{
|
||||
...options,
|
||||
where: {
|
||||
userId: this.id,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -8,6 +8,14 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#collections.add_user should not allow add self 1`] = `
|
||||
Object {
|
||||
"error": "authorization_error",
|
||||
"message": "Authorization error",
|
||||
"ok": false,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#collections.add_user should require user in team 1`] = `
|
||||
Object {
|
||||
"error": "authorization_error",
|
||||
|
||||
@@ -422,6 +422,24 @@ describe("#collections.add_user", () => {
|
||||
expect(users.length).toEqual(2);
|
||||
});
|
||||
|
||||
it("should not allow add self", async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
teamId: user.teamId,
|
||||
permission: null,
|
||||
});
|
||||
const res = await server.post("/api/collections.add_user", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: collection.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(403);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should require user in team", async () => {
|
||||
const user = await buildUser();
|
||||
const collection = await buildCollection({
|
||||
|
||||
@@ -9,7 +9,7 @@ import { RateLimiterStrategy } from "@server/RateLimiter";
|
||||
import collectionExporter from "@server/commands/collectionExporter";
|
||||
import teamUpdater from "@server/commands/teamUpdater";
|
||||
import { sequelize } from "@server/database/sequelize";
|
||||
import { ValidationError } from "@server/errors";
|
||||
import { AuthorizationError, ValidationError } from "@server/errors";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { rateLimiter } from "@server/middlewares/rateLimiter";
|
||||
import {
|
||||
@@ -362,6 +362,10 @@ router.post("collections.add_user", auth(), async (ctx) => {
|
||||
},
|
||||
});
|
||||
|
||||
if (userId === ctx.state.user.id) {
|
||||
throw AuthorizationError("You cannot add yourself to a collection");
|
||||
}
|
||||
|
||||
if (permission) {
|
||||
assertCollectionPermission(permission);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { FileOperation, Team } from "@server/models";
|
||||
import { FileOperationType } from "@server/models/FileOperation";
|
||||
import { authorize } from "@server/policies";
|
||||
import { presentFileOperation } from "@server/presenters";
|
||||
import { ContextWithState } from "@server/types";
|
||||
import { getSignedUrl } from "@server/utils/s3";
|
||||
import { assertIn, assertSort, assertUuid } from "@server/validation";
|
||||
import pagination from "./middlewares/pagination";
|
||||
@@ -68,8 +69,8 @@ router.post(
|
||||
}
|
||||
);
|
||||
|
||||
router.post("fileOperations.redirect", auth({ admin: true }), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
const handleFileOperationsRedirect = async (ctx: ContextWithState) => {
|
||||
const { id } = ctx.body as { id?: string };
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
const { user } = ctx.state;
|
||||
@@ -84,7 +85,18 @@ router.post("fileOperations.redirect", auth({ admin: true }), async (ctx) => {
|
||||
|
||||
const accessUrl = await getSignedUrl(fileOperation.key);
|
||||
ctx.redirect(accessUrl);
|
||||
});
|
||||
};
|
||||
|
||||
router.get(
|
||||
"fileOperations.redirect",
|
||||
auth({ admin: true }),
|
||||
handleFileOperationsRedirect
|
||||
);
|
||||
router.post(
|
||||
"fileOperations.redirect",
|
||||
auth({ admin: true }),
|
||||
handleFileOperationsRedirect
|
||||
);
|
||||
|
||||
router.post("fileOperations.delete", auth({ admin: true }), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
|
||||
@@ -402,6 +402,7 @@ router.post(
|
||||
rateLimiter(RateLimiterStrategy.TenPerHour),
|
||||
async (ctx) => {
|
||||
const { id, code = "" } = ctx.body;
|
||||
const actor = ctx.state.user;
|
||||
let user: User;
|
||||
|
||||
if (id) {
|
||||
@@ -410,13 +411,13 @@ router.post(
|
||||
rejectOnEmpty: true,
|
||||
});
|
||||
} else {
|
||||
user = ctx.state.user;
|
||||
user = actor;
|
||||
}
|
||||
authorize(user, "delete", user);
|
||||
authorize(actor, "delete", user);
|
||||
|
||||
// If we're attempting to delete our own account then a confirmation code
|
||||
// is required. This acts as CSRF protection.
|
||||
if (!id || id === ctx.state.user.id) {
|
||||
if (!id || id === actor.id) {
|
||||
const deleteConfirmationCode = user.deleteConfirmationCode;
|
||||
|
||||
if (
|
||||
@@ -433,7 +434,7 @@ router.post(
|
||||
|
||||
await userDestroyer({
|
||||
user,
|
||||
actor: user,
|
||||
actor,
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import path from "path";
|
||||
import Koa, { BaseContext } from "koa";
|
||||
import compress from "koa-compress";
|
||||
import Router from "koa-router";
|
||||
import send from "koa-send";
|
||||
import serve from "koa-static";
|
||||
import userAgent, { UserAgentContext } from "koa-useragent";
|
||||
import { languages } from "@shared/i18n";
|
||||
import env from "@server/env";
|
||||
@@ -16,15 +16,31 @@ const isProduction = env.ENVIRONMENT === "production";
|
||||
const koa = new Koa();
|
||||
const router = new Router();
|
||||
|
||||
// serve static assets
|
||||
koa.use(
|
||||
serve(path.resolve(__dirname, "../../../public"), {
|
||||
maxage: 60 * 60 * 24 * 30 * 1000,
|
||||
})
|
||||
);
|
||||
|
||||
koa.use<BaseContext, UserAgentContext>(userAgent);
|
||||
|
||||
// serve public assets
|
||||
router.use(["/images/*", "/email/*"], async (ctx, next) => {
|
||||
let done;
|
||||
|
||||
if (ctx.method === "HEAD" || ctx.method === "GET") {
|
||||
try {
|
||||
done = await send(ctx, ctx.path, {
|
||||
root: path.resolve(__dirname, "../../../public"),
|
||||
// 7 day expiry, these assets are mostly static but do not contain a hash
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.status !== 404) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!done) {
|
||||
await next();
|
||||
}
|
||||
});
|
||||
|
||||
if (isProduction) {
|
||||
router.get("/static/*", async (ctx) => {
|
||||
try {
|
||||
@@ -55,6 +71,8 @@ if (isProduction) {
|
||||
});
|
||||
}
|
||||
|
||||
router.use(compress());
|
||||
|
||||
router.get("/locales/:lng.json", async (ctx) => {
|
||||
const { lng } = ctx.params;
|
||||
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
href="/favicon-32.png"
|
||||
href="/images/favicon-32.png"
|
||||
sizes="32x32"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
type="image/png"
|
||||
href="/apple-touch-icon.png"
|
||||
href="/images/apple-touch-icon.png"
|
||||
sizes="192x192"
|
||||
/>
|
||||
<link
|
||||
|
||||
@@ -4,7 +4,7 @@ export const opensearchResponse = (baseUrl: string): string => {
|
||||
<ShortName>Outline</ShortName>
|
||||
<Description>Search Outline</Description>
|
||||
<InputEncoding>UTF-8</InputEncoding>
|
||||
<Image width="16" height="16" type="image/x-icon">${baseUrl}/favicon.ico</Image>
|
||||
<Image width="16" height="16" type="image/x-icon">${baseUrl}/images/favicon-16.png</Image>
|
||||
<Url type="text/html" method="get" template="${baseUrl}/search/{searchTerms}?ref=opensearch"/>
|
||||
<moz:SearchForm>${baseUrl}/search</moz:SearchForm>
|
||||
</OpenSearchDescription>
|
||||
|
||||
@@ -69,7 +69,7 @@ module.exports = {
|
||||
display: "standalone",
|
||||
icons: [
|
||||
{
|
||||
src: path.resolve("public/icon-512.png"),
|
||||
src: path.resolve("public/images/icon-512.png"),
|
||||
// For Chrome, you must provide at least a 192x192 pixel icon, and a 512x512 pixel icon.
|
||||
// If only those two icon sizes are provided, Chrome will automatically scale the icons
|
||||
// to fit the device. If you'd prefer to scale your own icons, and adjust them for
|
||||
|
||||
@@ -2806,7 +2806,7 @@
|
||||
dependencies:
|
||||
"@types/koa" "*"
|
||||
|
||||
"@types/koa-send@*":
|
||||
"@types/koa-send@^4.1.3":
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/koa-send/-/koa-send-4.1.3.tgz#17193c6472ae9e5d1b99ae8086949cc4fd69179d"
|
||||
integrity sha512-daaTqPZlgjIJycSTNjKpHYuKhXYP30atFc1pBcy6HHqB9+vcymDgYTguPdx9tO4HMOqNyz6bz/zqpxt5eLR+VA==
|
||||
@@ -2820,14 +2820,6 @@
|
||||
dependencies:
|
||||
"@types/koa" "*"
|
||||
|
||||
"@types/koa-static@^4.0.2":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/koa-static/-/koa-static-4.0.2.tgz#a199d2d64d2930755eb3ea370aeaf2cb6f501d67"
|
||||
integrity sha512-ns/zHg+K6XVPMuohjpOlpkR1WLa4VJ9czgUP9bxkCDn0JZBtUWbD/wKDZzPGDclkQK1bpAEScufCHOy8cbfL0w==
|
||||
dependencies:
|
||||
"@types/koa" "*"
|
||||
"@types/koa-send" "*"
|
||||
|
||||
"@types/koa-useragent@^2.1.2":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/koa-useragent/-/koa-useragent-2.1.2.tgz#7c75fe55c742e559c4643d65b34c6ce5945f853f"
|
||||
@@ -6055,7 +6047,7 @@ debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, d
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^2.2.0, debug@^2.3.3, debug@^2.6.1, debug@^2.6.3, debug@^2.6.8, debug@^2.6.9:
|
||||
debug@^2.2.0, debug@^2.3.3, debug@^2.6.1, debug@^2.6.8, debug@^2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
@@ -8322,7 +8314,7 @@ http-errors@2.0.0:
|
||||
statuses "2.0.1"
|
||||
toidentifier "1.0.1"
|
||||
|
||||
http-errors@^1.3.1, http-errors@^1.6.1, http-errors@^1.6.3, http-errors@^1.7.3, http-errors@^1.8.0:
|
||||
http-errors@^1.3.1, http-errors@^1.6.3, http-errors@^1.7.3, http-errors@^1.8.0:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c"
|
||||
integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==
|
||||
@@ -9957,29 +9949,11 @@ koa-send@5.0.1, koa-send@^5.0.0:
|
||||
http-errors "^1.7.3"
|
||||
resolve-path "^1.4.0"
|
||||
|
||||
koa-send@^4.1.3:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-4.1.3.tgz#0822207bbf5253a414c8f1765ebc29fa41353cb6"
|
||||
integrity sha512-3UetMBdaXSiw24qM2Mx5mKmxLKw5ZTPRjACjfhK6Haca55RKm9hr/uHDrkrxhSl5/S1CKI/RivZVIopiatZuTA==
|
||||
dependencies:
|
||||
debug "^2.6.3"
|
||||
http-errors "^1.6.1"
|
||||
mz "^2.6.0"
|
||||
resolve-path "^1.4.0"
|
||||
|
||||
koa-sslify@2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/koa-sslify/-/koa-sslify-2.1.2.tgz#8947fd53949d69d539607814097863c1ecf38f30"
|
||||
integrity sha1-iUf9U5SdadU5YHgUCXhjwezzjzA=
|
||||
|
||||
koa-static@^4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/koa-static/-/koa-static-4.0.3.tgz#5f93ad00fb1905db9ce46667c0e8bb7d22abfcd8"
|
||||
integrity sha512-JGmxTuPWy4bH7bt6gD/OMWkhprawvRmzJSr8TWKmTL4N7+IMv3s0SedeQi5S4ilxM9Bo6ptkCyXj/7wf+VS5tg==
|
||||
dependencies:
|
||||
debug "^3.1.0"
|
||||
koa-send "^4.1.3"
|
||||
|
||||
koa-static@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/koa-static/-/koa-static-5.0.0.tgz#5e92fc96b537ad5219f425319c95b64772776943"
|
||||
@@ -10925,7 +10899,7 @@ multer@^1.4.2:
|
||||
type-is "^1.6.4"
|
||||
xtend "^4.0.0"
|
||||
|
||||
mz@^2.4.0, mz@^2.6.0:
|
||||
mz@^2.4.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
|
||||
integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
|
||||
|
||||