mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2026-08-07 07:14:49 +00:00
feat(permissions): publiclyVisible flag for note listing on explore
This field should allow the user to allow pages to be found easily on the public explore page without exposing all pages directly. Co-authored-by: Erik Michelson <github@erik.michelson.eu> Signed-off-by: Erik Michelson <github@erik.michelson.eu> Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
@@ -23,6 +23,7 @@ import { ApiTags } from '@nestjs/swagger';
|
||||
import { SessionGuard } from '../../../auth/session.guard';
|
||||
import noteConfiguration, { NoteConfig } from '../../../config/note.config';
|
||||
import { ChangeNoteOwnerDto } from '../../../dtos/change-note-owner.dto';
|
||||
import { ChangeNoteVisibilityDto } from '../../../dtos/change-note-visibility.dto';
|
||||
import { MediaUploadDto } from '../../../dtos/media-upload.dto';
|
||||
import { NoteGroupPermissionEntryDto } from '../../../dtos/note-group-permission-entry.dto';
|
||||
import { NoteGroupPermissionUpdateDto } from '../../../dtos/note-group-permission-update.dto';
|
||||
@@ -267,11 +268,25 @@ export class NotesController {
|
||||
async changeOwner(
|
||||
@RequestNoteId() noteId: number,
|
||||
@Body() changeNoteOwnerDto: ChangeNoteOwnerDto,
|
||||
): Promise<NoteDto> {
|
||||
): Promise<NotePermissionsDto> {
|
||||
const newOwnerId = await this.userService.getUserIdByUsername(
|
||||
changeNoteOwnerDto.owner,
|
||||
);
|
||||
await this.permissionService.changeOwner(noteId, newOwnerId);
|
||||
return await this.noteService.toNoteDto(noteId);
|
||||
return await this.permissionService.getPermissionsDtoForNote(noteId);
|
||||
}
|
||||
|
||||
@UseInterceptors(GetNoteIdInterceptor)
|
||||
@RequirePermission(PermissionLevel.FULL)
|
||||
@Put(':noteAlias/metadata/permissions/visibility')
|
||||
async changeVisibility(
|
||||
@RequestNoteId() noteId: number,
|
||||
@Body() changeNoteVisibilityDto: ChangeNoteVisibilityDto,
|
||||
): Promise<NotePermissionsDto> {
|
||||
await this.permissionService.changePubliclyVisible(
|
||||
noteId,
|
||||
changeNoteVisibilityDto.publiclyVisible,
|
||||
);
|
||||
return await this.permissionService.getPermissionsDtoForNote(noteId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,6 +335,30 @@ export class NotesController {
|
||||
return await this.noteService.toNoteMetadataDto(noteId);
|
||||
}
|
||||
|
||||
@UseInterceptors(GetNoteIdInterceptor)
|
||||
@RequirePermission(PermissionLevel.FULL)
|
||||
@Put(':noteAlias/metadata/permissions/visibility')
|
||||
@OpenApi(
|
||||
{
|
||||
code: 200,
|
||||
description: 'Changes the owner of the note',
|
||||
schema: NoteSchema,
|
||||
},
|
||||
403,
|
||||
404,
|
||||
)
|
||||
async change(
|
||||
@RequestNoteId() noteId: number,
|
||||
@Body('newPubliclyVisible') newPubliclyVisible: boolean,
|
||||
): Promise<NoteMetadataDto> {
|
||||
await this.permissionService.changePubliclyVisible(
|
||||
noteId,
|
||||
newPubliclyVisible,
|
||||
);
|
||||
|
||||
return await this.noteService.toNoteMetadataDto(noteId);
|
||||
}
|
||||
|
||||
@UseInterceptors(GetNoteIdInterceptor)
|
||||
@RequirePermission(PermissionLevel.READ)
|
||||
@Get(':noteAlias/revisions')
|
||||
|
||||
@@ -18,6 +18,7 @@ export function createDefaultMockNoteConfig(): NoteConfig {
|
||||
default: {
|
||||
everyone: PermissionLevel.READ,
|
||||
loggedIn: PermissionLevel.WRITE,
|
||||
publiclyVisible: false,
|
||||
},
|
||||
},
|
||||
revisionRetentionDays: 0,
|
||||
|
||||
@@ -20,6 +20,7 @@ describe('noteConfig', () => {
|
||||
const wrongDefaultPermission = 'wrong';
|
||||
const retentionDays = 30;
|
||||
const persistInteval = 15;
|
||||
const publiclyVisible = true;
|
||||
|
||||
describe('correctly parses config', () => {
|
||||
it('when given correct and complete environment variables', () => {
|
||||
@@ -32,6 +33,8 @@ describe('noteConfig', () => {
|
||||
PermissionLevelNames[PermissionLevel.WRITE],
|
||||
HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN:
|
||||
PermissionLevelNames[PermissionLevel.WRITE],
|
||||
HD_NOTE_PERMISSIONS_DEFAULT_PUBLICLY_VISIBLE:
|
||||
publiclyVisible.toString(),
|
||||
HD_NOTE_REVISION_RETENTION_DAYS: retentionDays.toString(),
|
||||
HD_NOTE_PERSIST_INTERVAL: persistInteval.toString(),
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
@@ -50,6 +53,9 @@ describe('noteConfig', () => {
|
||||
expect(config.permissions.default.loggedIn).toEqual(
|
||||
PermissionLevel.WRITE,
|
||||
);
|
||||
expect(config.permissions.default.publiclyVisible).toEqual(
|
||||
publiclyVisible,
|
||||
);
|
||||
expect(config.permissions.maxGuestLevel).toEqual(guestAccess);
|
||||
expect(config.revisionRetentionDays).toEqual(retentionDays);
|
||||
expect(config.persistInterval).toEqual(persistInteval);
|
||||
@@ -80,6 +86,7 @@ describe('noteConfig', () => {
|
||||
expect(config.permissions.default.loggedIn).toEqual(
|
||||
PermissionLevel.WRITE,
|
||||
);
|
||||
expect(config.permissions.default.publiclyVisible).toEqual(false);
|
||||
expect(config.permissions.maxGuestLevel).toEqual(guestAccess);
|
||||
restore();
|
||||
});
|
||||
@@ -110,7 +117,7 @@ describe('noteConfig', () => {
|
||||
expect(config.permissions.default.loggedIn).toEqual(
|
||||
PermissionLevel.WRITE,
|
||||
);
|
||||
|
||||
expect(config.permissions.default.publiclyVisible).toEqual(false);
|
||||
expect(config.permissions.maxGuestLevel).toEqual(guestAccess);
|
||||
restore();
|
||||
});
|
||||
@@ -140,7 +147,7 @@ describe('noteConfig', () => {
|
||||
expect(config.permissions.default.loggedIn).toEqual(
|
||||
PermissionLevel.WRITE,
|
||||
);
|
||||
|
||||
expect(config.permissions.default.publiclyVisible).toEqual(false);
|
||||
expect(config.permissions.maxGuestLevel).toEqual(guestAccess);
|
||||
restore();
|
||||
});
|
||||
@@ -167,7 +174,7 @@ describe('noteConfig', () => {
|
||||
expect(config.maxLength).toEqual(maxLength);
|
||||
expect(config.permissions.default.everyone).toEqual(PermissionLevel.READ);
|
||||
expect(config.permissions.default.loggedIn).toEqual(PermissionLevel.READ);
|
||||
|
||||
expect(config.permissions.default.publiclyVisible).toEqual(false);
|
||||
expect(config.permissions.maxGuestLevel).toEqual(PermissionLevel.READ);
|
||||
restore();
|
||||
});
|
||||
@@ -196,7 +203,7 @@ describe('noteConfig', () => {
|
||||
expect(config.permissions.default.loggedIn).toEqual(
|
||||
PermissionLevel.WRITE,
|
||||
);
|
||||
|
||||
expect(config.permissions.default.publiclyVisible).toEqual(false);
|
||||
expect(config.permissions.maxGuestLevel).toEqual(guestAccess);
|
||||
restore();
|
||||
});
|
||||
@@ -225,6 +232,7 @@ describe('noteConfig', () => {
|
||||
expect(config.permissions.default.loggedIn).toEqual(
|
||||
PermissionLevel.WRITE,
|
||||
);
|
||||
expect(config.permissions.default.publiclyVisible).toEqual(false);
|
||||
expect(config.permissions.maxGuestLevel).toEqual(PermissionLevel.FULL);
|
||||
restore();
|
||||
});
|
||||
@@ -255,6 +263,7 @@ describe('noteConfig', () => {
|
||||
expect(config.permissions.default.loggedIn).toEqual(
|
||||
PermissionLevel.WRITE,
|
||||
);
|
||||
expect(config.permissions.default.publiclyVisible).toEqual(false);
|
||||
expect(config.permissions.maxGuestLevel).toEqual(guestAccess);
|
||||
expect(config.revisionRetentionDays).toEqual(0);
|
||||
restore();
|
||||
@@ -286,6 +295,7 @@ describe('noteConfig', () => {
|
||||
expect(config.permissions.default.loggedIn).toEqual(
|
||||
PermissionLevel.WRITE,
|
||||
);
|
||||
expect(config.permissions.default.publiclyVisible).toEqual(false);
|
||||
expect(config.persistInterval).toEqual(10);
|
||||
restore();
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { registerAs } from '@nestjs/config';
|
||||
import z from 'zod';
|
||||
|
||||
import {
|
||||
parseOptionalBoolean,
|
||||
parseOptionalNumber,
|
||||
printConfigErrorAndExit,
|
||||
toArrayConfig,
|
||||
@@ -21,64 +22,67 @@ import {
|
||||
extractDescriptionFromZodIssue,
|
||||
} from './zod-error-message';
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
forbiddenAliases: z
|
||||
.array(z.string().min(1))
|
||||
const schema = z.object({
|
||||
forbiddenAliases: z
|
||||
.array(z.string().min(1))
|
||||
.optional()
|
||||
.default([])
|
||||
.describe('HD_NOTE_FORBIDDEN_ALIASES'),
|
||||
maxLength: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.default(100000)
|
||||
.describe('HD_NOTE_MAX_LENGTH'),
|
||||
permissions: z.object({
|
||||
maxGuestLevel: z
|
||||
.enum(PermissionLevelNames)
|
||||
.optional()
|
||||
.default([])
|
||||
.describe('HD_NOTE_FORBIDDEN_ALIASES'),
|
||||
maxLength: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.default(100000)
|
||||
.describe('HD_NOTE_MAX_LENGTH'),
|
||||
permissions: z.object({
|
||||
maxGuestLevel: z
|
||||
.default(PermissionLevelNames[PermissionLevel.FULL])
|
||||
.describe('HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL')
|
||||
.transform((value) => PermissionLevelValues[value]),
|
||||
default: z.object({
|
||||
everyone: z
|
||||
.enum(PermissionLevelNames)
|
||||
.optional()
|
||||
.default(PermissionLevelNames[PermissionLevel.FULL])
|
||||
.describe('HD_NOTE_PERMISSIONS_MAX_GUEST_LEVEL')
|
||||
.default(PermissionLevelNames[PermissionLevel.READ])
|
||||
.describe('HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE')
|
||||
.refine((value) => {
|
||||
// The PermissionLevel.FULL is reserved for owner permissions in the note context and is therefore forbidden
|
||||
return value !== PermissionLevelNames[PermissionLevel.FULL];
|
||||
})
|
||||
.transform((value) => PermissionLevelValues[value]),
|
||||
default: z.object({
|
||||
everyone: z
|
||||
.enum(PermissionLevelNames)
|
||||
.optional()
|
||||
.default(PermissionLevelNames[PermissionLevel.READ])
|
||||
.describe('HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE')
|
||||
.refine((value) => {
|
||||
// The PermissionLevel.FULL is reserved for owner permissions in the note context and is therefore forbidden
|
||||
return value !== PermissionLevelNames[PermissionLevel.FULL];
|
||||
})
|
||||
.transform((value) => PermissionLevelValues[value]),
|
||||
loggedIn: z
|
||||
.enum(PermissionLevelNames)
|
||||
.optional()
|
||||
.default(PermissionLevelNames[PermissionLevel.WRITE])
|
||||
.describe('HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN')
|
||||
.refine((value) => {
|
||||
// The PermissionLevel.FULL is reserved for owner permissions in the note context and is therefore forbidden
|
||||
return value !== PermissionLevelNames[PermissionLevel.FULL];
|
||||
})
|
||||
.transform((value) => PermissionLevelValues[value]),
|
||||
}),
|
||||
loggedIn: z
|
||||
.enum(PermissionLevelNames)
|
||||
.optional()
|
||||
.default(PermissionLevelNames[PermissionLevel.WRITE])
|
||||
.describe('HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN')
|
||||
.refine((value) => {
|
||||
// The PermissionLevel.FULL is reserved for owner permissions in the note context and is therefore forbidden
|
||||
return value !== PermissionLevelNames[PermissionLevel.FULL];
|
||||
})
|
||||
.transform((value) => PermissionLevelValues[value]),
|
||||
publiclyVisible: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe('HD_NOTE_PERMISSIONS_DEFAULT_PUBLICLY_VISIBLE'),
|
||||
}),
|
||||
revisionRetentionDays: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.optional()
|
||||
.default(0)
|
||||
.describe('HD_NOTE_REVISION_RETENTION_DAYS'),
|
||||
persistInterval: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.default(10)
|
||||
.describe('HD_NOTE_PERSIST_INTERVAL'),
|
||||
})
|
||||
}),
|
||||
revisionRetentionDays: z
|
||||
.number()
|
||||
.int()
|
||||
.nonnegative()
|
||||
.optional()
|
||||
.default(0)
|
||||
.describe('HD_NOTE_REVISION_RETENTION_DAYS'),
|
||||
persistInterval: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.default(10)
|
||||
.describe('HD_NOTE_PERSIST_INTERVAL'),
|
||||
})
|
||||
.superRefine((config, ctx) => {
|
||||
const defaultEveryone = config.permissions.default.everyone;
|
||||
const defaultLoggedIn = config.permissions.default.loggedIn;
|
||||
@@ -120,6 +124,9 @@ export default registerAs('noteConfig', () => {
|
||||
default: {
|
||||
everyone: process.env.HD_NOTE_PERMISSIONS_DEFAULT_EVERYONE,
|
||||
loggedIn: process.env.HD_NOTE_PERMISSIONS_DEFAULT_LOGGED_IN,
|
||||
publiclyVisible: parseOptionalBoolean(
|
||||
process.env.HD_NOTE_PERMISSIONS_DEFAULT_PUBLICLY_VISIBLE,
|
||||
),
|
||||
},
|
||||
},
|
||||
revisionRetentionDays: parseOptionalNumber(
|
||||
@@ -128,6 +135,7 @@ export default registerAs('noteConfig', () => {
|
||||
persistInterval: parseOptionalNumber(process.env.HD_NOTE_PERSIST_INTERVAL),
|
||||
});
|
||||
if (noteConfig.error) {
|
||||
console.log(noteConfig.error);
|
||||
const errorMessages = noteConfig.error.errors.map((issue) =>
|
||||
extractDescriptionFromZodIssue(issue, 'HD_NOTE'),
|
||||
);
|
||||
|
||||
@@ -92,6 +92,7 @@ const up = async function (knex) {
|
||||
.references(FieldNameUser.id)
|
||||
.inTable(TableUser)
|
||||
.onDelete('CASCADE');
|
||||
table.boolean(FieldNameNote.publiclyVisible).notNullable();
|
||||
table.index([FieldNameNote.ownerId], 'idx_note_owner_id');
|
||||
});
|
||||
|
||||
|
||||
@@ -71,16 +71,19 @@ export async function seed(knex: Knex): Promise<void> {
|
||||
[FieldNameNote.ownerId]: 1,
|
||||
[FieldNameNote.version]: 2,
|
||||
[FieldNameNote.createdAt]: dateTimeToDB(getCurrentDateTime()),
|
||||
[FieldNameNote.publiclyVisible]: false,
|
||||
},
|
||||
{
|
||||
[FieldNameNote.ownerId]: 2,
|
||||
[FieldNameNote.version]: 2,
|
||||
[FieldNameNote.createdAt]: dateTimeToDB(getCurrentDateTime()),
|
||||
[FieldNameNote.publiclyVisible]: false,
|
||||
},
|
||||
{
|
||||
[FieldNameNote.ownerId]: 2,
|
||||
[FieldNameNote.version]: 2,
|
||||
[FieldNameNote.createdAt]: dateTimeToDB(getCurrentDateTime()),
|
||||
[FieldNameNote.publiclyVisible]: false,
|
||||
},
|
||||
]);
|
||||
await knex(TableAlias).insert([
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { ChangeNoteVisibilitySchema } from '@hedgedoc/commons';
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
|
||||
export class ChangeNoteVisibilityDto extends createZodDto(
|
||||
ChangeNoteVisibilitySchema,
|
||||
) {}
|
||||
@@ -263,35 +263,43 @@ describe('ExploreService', () => {
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid", "revision_tag"."tag" as "tag" from "note" inner join "note_group_permission" on "note"."id" = "note_group_permission"."note_id" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" left join "revision_tag" on "revision_tag"."revision_id" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note_group_permission"."group_id" = \$3 order by "revision"."created_at" desc limit \$4/,
|
||||
[1, true, mockEveryoneGroupId, ENTRIES_PER_PAGE_LIMIT],
|
||||
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid", "revision_tag"."tag" as "tag" from "note" inner join "note_group_permission" on "note"."id" = "note_group_permission"."note_id" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" left join "revision_tag" on "revision_tag"."revision_id" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note_group_permission"."group_id" = \$3 and "note"."publicly_visible" = \$4 order by "revision"."created_at" desc limit \$5/,
|
||||
[1, true, mockEveryoneGroupId, true, ENTRIES_PER_PAGE_LIMIT],
|
||||
],
|
||||
[
|
||||
'type filter',
|
||||
NoteType.SLIDE,
|
||||
'',
|
||||
'',
|
||||
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid", "revision_tag"."tag" as "tag" from "note" inner join "note_group_permission" on "note"."id" = "note_group_permission"."note_id" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" left join "revision_tag" on "revision_tag"."revision_id" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note_group_permission"."group_id" = \$3 and "revision"."note_type" = \$4 order by "revision"."created_at" desc limit \$5/,
|
||||
[1, true, mockEveryoneGroupId, NoteType.SLIDE, ENTRIES_PER_PAGE_LIMIT],
|
||||
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid", "revision_tag"."tag" as "tag" from "note" inner join "note_group_permission" on "note"."id" = "note_group_permission"."note_id" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" left join "revision_tag" on "revision_tag"."revision_id" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note_group_permission"."group_id" = \$3 and "note"."publicly_visible" = \$4 and "revision"."note_type" = \$5 order by "revision"."created_at" desc limit \$6/,
|
||||
[
|
||||
1,
|
||||
true,
|
||||
mockEveryoneGroupId,
|
||||
true,
|
||||
NoteType.SLIDE,
|
||||
ENTRIES_PER_PAGE_LIMIT,
|
||||
],
|
||||
],
|
||||
[
|
||||
'sorting',
|
||||
'',
|
||||
SortMode.TITLE_ASC,
|
||||
'',
|
||||
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid", "revision_tag"."tag" as "tag" from "note" inner join "note_group_permission" on "note"."id" = "note_group_permission"."note_id" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" left join "revision_tag" on "revision_tag"."revision_id" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note_group_permission"."group_id" = \$3 order by "revision"."title" asc limit \$4/,
|
||||
[1, true, mockEveryoneGroupId, ENTRIES_PER_PAGE_LIMIT],
|
||||
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid", "revision_tag"."tag" as "tag" from "note" inner join "note_group_permission" on "note"."id" = "note_group_permission"."note_id" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" left join "revision_tag" on "revision_tag"."revision_id" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note_group_permission"."group_id" = \$3 and "note"."publicly_visible" = \$4 order by "revision"."title" asc limit \$5/,
|
||||
[1, true, mockEveryoneGroupId, true, ENTRIES_PER_PAGE_LIMIT],
|
||||
],
|
||||
[
|
||||
'search filter',
|
||||
'',
|
||||
'',
|
||||
'test',
|
||||
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid", "revision_tag"."tag" as "tag" from "note" inner join "note_group_permission" on "note"."id" = "note_group_permission"."note_id" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" left join "revision_tag" on "revision_tag"."revision_id" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note_group_permission"."group_id" = \$3 and \(LOWER\("revision"."title"\) LIKE \$4 OR LOWER\("revision_tag"."tag"\) LIKE \$5\) order by "revision"."created_at" desc limit \$6/,
|
||||
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid", "revision_tag"."tag" as "tag" from "note" inner join "note_group_permission" on "note"."id" = "note_group_permission"."note_id" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" left join "revision_tag" on "revision_tag"."revision_id" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note_group_permission"."group_id" = \$3 and "note"."publicly_visible" = \$4 and \(LOWER\("revision"."title"\) LIKE \$5 OR LOWER\("revision_tag"."tag"\) LIKE \$6\) order by "revision"."created_at" desc limit \$7/,
|
||||
[
|
||||
1,
|
||||
true,
|
||||
mockEveryoneGroupId,
|
||||
true,
|
||||
'%test%',
|
||||
'%test%',
|
||||
ENTRIES_PER_PAGE_LIMIT,
|
||||
|
||||
@@ -162,10 +162,12 @@ export class ExploreService {
|
||||
`${TableNoteGroupPermission}.${FieldNameNoteGroupPermission.noteId}`,
|
||||
);
|
||||
let query = this.applyCommonQuery(queryBase);
|
||||
query = query.andWhere(
|
||||
`${TableNoteGroupPermission}.${FieldNameNoteGroupPermission.groupId}`,
|
||||
everyoneGroupId,
|
||||
);
|
||||
query = query
|
||||
.andWhere(
|
||||
`${TableNoteGroupPermission}.${FieldNameNoteGroupPermission.groupId}`,
|
||||
everyoneGroupId,
|
||||
)
|
||||
.andWhere(`${TableNote}.${FieldNameNote.publiclyVisible}`, true);
|
||||
if (
|
||||
sortBy === SortMode.LAST_VISITED_ASC ||
|
||||
sortBy === SortMode.LAST_VISITED_DESC
|
||||
|
||||
@@ -83,6 +83,7 @@ describe('NoteService', () => {
|
||||
const mockTags = ['tag1', 'tag2'];
|
||||
const mockPermissions = {
|
||||
owner: mockUsername,
|
||||
publiclyVisible: false,
|
||||
sharedToUsers: [],
|
||||
sharedToGroups: [],
|
||||
};
|
||||
@@ -172,14 +173,24 @@ describe('NoteService', () => {
|
||||
mockInsert(
|
||||
tracker,
|
||||
TableNote,
|
||||
[FieldNameNote.createdAt, FieldNameNote.ownerId, FieldNameNote.version],
|
||||
[
|
||||
FieldNameNote.createdAt,
|
||||
FieldNameNote.ownerId,
|
||||
FieldNameNote.publiclyVisible,
|
||||
FieldNameNote.version,
|
||||
],
|
||||
[],
|
||||
);
|
||||
await expect(
|
||||
service.createNote(mockNoteContent, mockOwnerUserId, mockAliasCustom),
|
||||
).rejects.toThrow(GenericDBError);
|
||||
expectBindings(tracker, 'insert', [
|
||||
[dateTimeToDB(now), mockOwnerUserId, 2],
|
||||
[
|
||||
dateTimeToDB(now),
|
||||
mockOwnerUserId,
|
||||
noteMockConfig.permissions.default.publiclyVisible,
|
||||
2,
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -246,6 +257,7 @@ describe('NoteService', () => {
|
||||
[
|
||||
FieldNameNote.createdAt,
|
||||
FieldNameNote.ownerId,
|
||||
FieldNameNote.publiclyVisible,
|
||||
FieldNameNote.version,
|
||||
],
|
||||
[{ [FieldNameNote.id]: mockNoteId }],
|
||||
@@ -260,7 +272,12 @@ describe('NoteService', () => {
|
||||
);
|
||||
expect(result).toBe(mockNoteId);
|
||||
expectBindings(tracker, 'insert', [
|
||||
[dateTimeToDB(now), mockOwnerUserId, 2],
|
||||
[
|
||||
dateTimeToDB(now),
|
||||
mockOwnerUserId,
|
||||
noteMockConfig.permissions.default.publiclyVisible,
|
||||
2,
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -107,6 +107,8 @@ export class NoteService {
|
||||
[FieldNameNote.ownerId]: ownerUserId,
|
||||
[FieldNameNote.version]: 2,
|
||||
[FieldNameNote.createdAt]: createdAt,
|
||||
[FieldNameNote.publiclyVisible]:
|
||||
this.noteConfig.permissions.default.publiclyVisible,
|
||||
},
|
||||
[FieldNameNote.id],
|
||||
);
|
||||
|
||||
@@ -11,12 +11,14 @@ import {
|
||||
FieldNameNoteGroupPermission,
|
||||
FieldNameNoteUserPermission,
|
||||
FieldNameUser,
|
||||
Note,
|
||||
TableGroup,
|
||||
TableMediaUpload,
|
||||
TableNote,
|
||||
TableNoteGroupPermission,
|
||||
TableNoteUserPermission,
|
||||
TableUser,
|
||||
User,
|
||||
} from '@hedgedoc/database';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
@@ -399,6 +401,27 @@ export class PermissionService {
|
||||
this.notifyOthers(noteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates if a note is publicly visible or not
|
||||
*
|
||||
* @param noteId the id of note to update
|
||||
* @param newPublicVisible the new state of the note
|
||||
* @throws NotInDBError if note does not exist
|
||||
*/
|
||||
public async changePubliclyVisible(
|
||||
noteId: number,
|
||||
newPublicVisible: boolean,
|
||||
): Promise<void> {
|
||||
const result = await this.knex(TableNote)
|
||||
.update({
|
||||
[FieldNameNote.publiclyVisible]: newPublicVisible,
|
||||
})
|
||||
.where(FieldNameNote.id, noteId);
|
||||
if (result !== 1) {
|
||||
throw new NotInDBError('The note does not exist');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the permissions for a note
|
||||
*
|
||||
@@ -423,15 +446,16 @@ export class PermissionService {
|
||||
noteId: number,
|
||||
transaction: Knex,
|
||||
): Promise<NotePermissionsDto> {
|
||||
const owner = await transaction(TableNote)
|
||||
const metadata = await transaction(TableNote)
|
||||
.join(
|
||||
TableUser,
|
||||
`${TableUser}.${FieldNameUser.id}`,
|
||||
`${TableNote}.${FieldNameNote.ownerId}`,
|
||||
)
|
||||
.select<{
|
||||
[FieldNameUser.username]: string;
|
||||
}>(`${TableUser}.${FieldNameUser.username}`)
|
||||
.select<
|
||||
Pick<User, FieldNameUser.username> &
|
||||
Pick<Note, FieldNameNote.publiclyVisible>
|
||||
>(`${TableUser}.${FieldNameUser.username}`, `${TableNote}.${FieldNameNote.publiclyVisible}`)
|
||||
.where(`${TableNote}.${FieldNameNote.id}`, noteId)
|
||||
.first();
|
||||
|
||||
@@ -475,7 +499,7 @@ export class PermissionService {
|
||||
noteId,
|
||||
);
|
||||
|
||||
if (owner === undefined) {
|
||||
if (metadata === undefined) {
|
||||
throw new GenericDBError(
|
||||
'Invalid database state. This should not happen.',
|
||||
this.logger.getContext(),
|
||||
@@ -484,7 +508,8 @@ export class PermissionService {
|
||||
}
|
||||
|
||||
return NotePermissionsDto.create({
|
||||
owner: owner[FieldNameUser.username],
|
||||
owner: metadata[FieldNameUser.username],
|
||||
publiclyVisible: Boolean(metadata[FieldNameNote.publiclyVisible]),
|
||||
sharedToUsers: userPermissions.map((userPermission) => ({
|
||||
username: userPermission[FieldNameUser.username],
|
||||
canEdit: Boolean(userPermission[FieldNameNoteUserPermission.canEdit]),
|
||||
|
||||
@@ -529,12 +529,40 @@ describe('PermissionsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('changePubliclyVisibly', () => {
|
||||
// eslint-disable-next-line func-style
|
||||
const buildMockUpdate = (updatedEntries: number) => {
|
||||
mockUpdate(
|
||||
tracker,
|
||||
TableNote,
|
||||
[FieldNameNote.publiclyVisible],
|
||||
FieldNameNote.id,
|
||||
updatedEntries,
|
||||
);
|
||||
};
|
||||
it('throws NotInDBError when the update does not succed', async () => {
|
||||
buildMockUpdate(0);
|
||||
await expect(
|
||||
service.changePubliclyVisible(mockNoteId, true),
|
||||
).rejects.toThrow(NotInDBError);
|
||||
expectBindings(tracker, 'update', [[true, mockNoteId]]);
|
||||
});
|
||||
it('correctly notifies others', async () => {
|
||||
buildMockUpdate(1);
|
||||
await service.changePubliclyVisible(mockNoteId, true);
|
||||
expectBindings(tracker, 'update', [[true, mockNoteId]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPermissionsDtoForNote', () => {
|
||||
// eslint-disable-next-line func-style
|
||||
const buildMockOwnerSelect = (returnValues: unknown) => {
|
||||
mockSelect(
|
||||
tracker,
|
||||
[`${TableUser}"."${FieldNameUser.username}`],
|
||||
[
|
||||
`${TableUser}"."${FieldNameUser.username}`,
|
||||
`${TableNote}"."${FieldNameNote.publiclyVisible}`,
|
||||
],
|
||||
TableNote,
|
||||
`${TableNote}"."${FieldNameNote.id}`,
|
||||
returnValues,
|
||||
@@ -616,6 +644,8 @@ describe('PermissionsService', () => {
|
||||
buildMockOwnerSelect([
|
||||
{
|
||||
[FieldNameUser.username]: mockUserName2,
|
||||
[FieldNameNote.publiclyVisible]:
|
||||
noteMockConfig.permissions.default.publiclyVisible,
|
||||
},
|
||||
]);
|
||||
const results = await service.getPermissionsDtoForNote(mockNoteId);
|
||||
|
||||
@@ -415,6 +415,7 @@ describe('Notes', () => {
|
||||
expect(typeof metadataBody.createdAt).toEqual('string');
|
||||
expect(metadataBody.editedBy).toEqual([username1]);
|
||||
expect(metadataBody.permissions.owner).toEqual(username1);
|
||||
expect(metadataBody.permissions.publiclyVisible).toEqual(false);
|
||||
expect(metadataBody.permissions.sharedToUsers).toEqual([]);
|
||||
expect(metadataBody.permissions.sharedToGroups).toEqual([
|
||||
{
|
||||
@@ -507,7 +508,6 @@ describe('Notes', () => {
|
||||
});
|
||||
afterEach(() => {
|
||||
expect(response.body).toHaveLength(2);
|
||||
console.log(response.body);
|
||||
expect(response.body[0].length).toEqual(content.length);
|
||||
expect(response.body[1].length).toEqual(noteContent1.length);
|
||||
});
|
||||
@@ -756,6 +756,7 @@ describe('Notes', () => {
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`DELETE ${PRIVATE_API_PREFIX}/notes/:noteAlias/metadata/permissions/users/:username`, () => {
|
||||
beforeEach(async () => {
|
||||
const noteId = testSetup.ownedNoteIds[0];
|
||||
@@ -823,6 +824,7 @@ describe('Notes', () => {
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`PUT ${PRIVATE_API_PREFIX}/notes/:noteAlias/metadata/permissions/groups/:groupName`, () => {
|
||||
beforeEach(async () => {
|
||||
const noteId: number = testSetup.ownedNoteIds[0];
|
||||
@@ -923,6 +925,7 @@ describe('Notes', () => {
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`DELETE ${PRIVATE_API_PREFIX}/notes/:noteAlias/metadata/permissions/groups/:groupName`, () => {
|
||||
beforeEach(async () => {
|
||||
const noteId = testSetup.ownedNoteIds[0];
|
||||
@@ -990,6 +993,7 @@ describe('Notes', () => {
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`PUT ${PRIVATE_API_PREFIX}/notes/:noteAlias/metadata/permissions/owner`, () => {
|
||||
it('can change owner as the owner', async () => {
|
||||
const response = await agentUser1
|
||||
@@ -1001,7 +1005,7 @@ describe('Notes', () => {
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
expect(response.body.metadata.permissions.owner).toEqual(username2);
|
||||
expect(response.body.owner).toEqual(username2);
|
||||
});
|
||||
describe("can't change owner as", () => {
|
||||
it('another user', async () => {
|
||||
@@ -1061,4 +1065,76 @@ describe('Notes', () => {
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`PUT ${PRIVATE_API_PREFIX}/notes/:noteAlias/metadata/permissions/visibility`, () => {
|
||||
it('can change visibility as the owner', async () => {
|
||||
const response = await agentUser1
|
||||
.put(
|
||||
`${PRIVATE_API_PREFIX}/notes/${noteAlias1}/metadata/permissions/visibility`,
|
||||
)
|
||||
.send({
|
||||
publiclyVisible: true,
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
expect(response.body.publiclyVisible).toEqual(true);
|
||||
});
|
||||
describe("can't change visibility as", () => {
|
||||
it('another user', async () => {
|
||||
await agentUser2
|
||||
.put(
|
||||
`${PRIVATE_API_PREFIX}/notes/${noteAlias1}/metadata/permissions/visibility`,
|
||||
)
|
||||
.send({
|
||||
publiclyVisible: true,
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(403);
|
||||
});
|
||||
it('guest user', async () => {
|
||||
await agentGuestUser
|
||||
.put(
|
||||
`${PRIVATE_API_PREFIX}/notes/${noteAlias1}/metadata/permissions/visibility`,
|
||||
)
|
||||
.send({
|
||||
publiclyVisible: true,
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(403);
|
||||
});
|
||||
it('not logged-in user', async () => {
|
||||
await agentNotLoggedIn
|
||||
.put(
|
||||
`${PRIVATE_API_PREFIX}/notes/${noteAlias1}/metadata/permissions/visibility`,
|
||||
)
|
||||
.send({
|
||||
publiclyVisible: true,
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
it('throws an error if using a forbidden alias', async () => {
|
||||
await agentUser1
|
||||
.put(
|
||||
`${PRIVATE_API_PREFIX}/notes/${forbiddenAlias}/metadata/permissions/visibility`,
|
||||
)
|
||||
.send({
|
||||
publiclyVisible: true,
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(403);
|
||||
});
|
||||
it('throws an error if using a non-existing alias', async () => {
|
||||
await agentUser1
|
||||
.put(
|
||||
`${PRIVATE_API_PREFIX}/notes/i_do_not_exist/metadata/permissions/visibility`,
|
||||
)
|
||||
.send({
|
||||
publiclyVisible: true,
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -415,7 +415,18 @@ describe('Notes', () => {
|
||||
expect(typeof response.body.createdAt).toEqual('string');
|
||||
expect(response.body.editedBy).toEqual([username1]);
|
||||
expect(response.body.permissions.owner).toEqual(username1);
|
||||
expect(response.body.permissions.publiclyVisible).toEqual(false);
|
||||
expect(response.body.permissions.sharedToUsers).toEqual([]);
|
||||
expect(response.body.permissions.sharedToGroups).toEqual([
|
||||
{
|
||||
groupName: SpecialGroup.EVERYONE,
|
||||
canEdit: false,
|
||||
},
|
||||
{
|
||||
groupName: SpecialGroup.LOGGED_IN,
|
||||
canEdit: true,
|
||||
},
|
||||
]);
|
||||
expect(response.body.tags).toEqual([]);
|
||||
expect(typeof response.body.updatedAt).toEqual('string');
|
||||
expect(typeof response.body.lastUpdatedBy).toEqual('string');
|
||||
@@ -483,6 +494,7 @@ describe('Notes', () => {
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
expect(permissions.body.owner).toBe(username1);
|
||||
expect(permissions.body.publiclyVisible).toBe(false);
|
||||
expect(new Set(permissions.body.sharedToUsers)).toEqual(new Set([]));
|
||||
expect(new Set(permissions.body.sharedToGroups)).toEqual(
|
||||
new Set([
|
||||
@@ -927,6 +939,80 @@ describe('Notes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(`PUT ${PUBLIC_API_PREFIX}/notes/{:noteAlias}/metadata/permissions/visibility`, () => {
|
||||
it('changes visibility of a note', async () => {
|
||||
const permissionsDtoBefore =
|
||||
await testSetup.permissionsService.getPermissionsDtoForNote(
|
||||
testSetup.ownedNoteIds[0],
|
||||
);
|
||||
expect(permissionsDtoBefore.publiclyVisible).toEqual(false);
|
||||
|
||||
await agent
|
||||
.put(
|
||||
`${PUBLIC_API_PREFIX}/notes/${noteAlias1}/metadata/permissions/visibility`,
|
||||
)
|
||||
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
|
||||
.send({
|
||||
newPubliclyVisible: true,
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
|
||||
const permissionsDtoAfter =
|
||||
await testSetup.permissionsService.getPermissionsDtoForNote(
|
||||
testSetup.ownedNoteIds[0],
|
||||
);
|
||||
expect(permissionsDtoAfter.publiclyVisible).toEqual(true);
|
||||
});
|
||||
it('errors with a forbidden alias', async () => {
|
||||
await agent
|
||||
.put(
|
||||
`${PUBLIC_API_PREFIX}/notes/${forbiddenAlias}/metadata/permissions/visibility`,
|
||||
)
|
||||
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
|
||||
.send({
|
||||
newPubliclyVisible: true,
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(403);
|
||||
});
|
||||
it('errors with non-existing alias', async () => {
|
||||
await agent
|
||||
.put(
|
||||
`${PUBLIC_API_PREFIX}/notes/i_dont_exist/metadata/permissions/visibility`,
|
||||
)
|
||||
.set('Authorization', `Bearer ${testSetup.authTokens[0].secret}`)
|
||||
.send({
|
||||
newPubliclyVisible: true,
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(404);
|
||||
});
|
||||
it('errors if no token is provided', async () => {
|
||||
await agent
|
||||
.put(
|
||||
`${PUBLIC_API_PREFIX}/notes/${noteAlias1}/metadata/permissions/visibility`,
|
||||
)
|
||||
.send({
|
||||
newPubliclyVisible: true,
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(403);
|
||||
});
|
||||
it("errors when user can't access note", async () => {
|
||||
await agent
|
||||
.put(
|
||||
`${PUBLIC_API_PREFIX}/notes/${noteAlias1}/metadata/permissions/visibility`,
|
||||
)
|
||||
.set('Authorization', `Bearer ${testSetup.authTokens[1].secret}`)
|
||||
.send({
|
||||
newPubliclyVisible: true,
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`GET ${PUBLIC_API_PREFIX}/notes/{:noteAlias}/revisions`, () => {
|
||||
it('works with existing alias', async () => {
|
||||
const response = await agent
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { z } from 'zod'
|
||||
|
||||
export const ChangeNoteVisibilitySchema = z
|
||||
.object({
|
||||
publiclyVisible: z
|
||||
.boolean()
|
||||
.describe('Whether the note should be listed on the public explore page'),
|
||||
})
|
||||
.describe('DTO to change the visibility of a note.')
|
||||
|
||||
export type ChangeNoteVisibilityInterface = z.infer<
|
||||
typeof ChangeNoteVisibilitySchema
|
||||
>
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
export * from './change-note-owner.dto.js'
|
||||
export * from './change-note-visibility.dto.js'
|
||||
export * from './note-group-permission-entry.dto.js'
|
||||
export * from './note-group-permission-update.dto.js'
|
||||
export * from './note-permissions-update.dto.js'
|
||||
|
||||
@@ -11,6 +11,7 @@ import { NoteGroupPermissionEntrySchema } from './note-group-permission-entry.dt
|
||||
export const NotePermissionsSchema = z
|
||||
.object({
|
||||
owner: z.string().nullable().describe('Username of the owner of the note'),
|
||||
publiclyVisible: z.boolean().describe('If a note is publicly visible'),
|
||||
sharedToUsers: z
|
||||
.array(NoteUserPermissionEntrySchema)
|
||||
.describe('List of users the note is shared with'),
|
||||
|
||||
@@ -10,6 +10,7 @@ import { NotePermissionsInterface, SpecialGroup } from '../dtos/index.js'
|
||||
describe('Permissions', () => {
|
||||
const testPermissions: NotePermissionsInterface = {
|
||||
owner: 'owner',
|
||||
publiclyVisible: true,
|
||||
sharedToUsers: [
|
||||
{
|
||||
username: 'logged_in',
|
||||
|
||||
@@ -21,6 +21,9 @@ export interface Note {
|
||||
|
||||
/** Timestamp when the note was created */
|
||||
[FieldNameNote.createdAt]: string
|
||||
|
||||
/** If a note should be visible in the public notes section */
|
||||
[FieldNameNote.publiclyVisible]: boolean
|
||||
}
|
||||
|
||||
export enum FieldNameNote {
|
||||
@@ -28,9 +31,12 @@ export enum FieldNameNote {
|
||||
ownerId = 'owner_id',
|
||||
version = 'version',
|
||||
createdAt = 'created_at',
|
||||
publiclyVisible = 'publicly_visible',
|
||||
}
|
||||
|
||||
export const TableNote = 'note'
|
||||
|
||||
export type TypeInsertNote = Omit<Note, FieldNameNote.id>
|
||||
export type TypeUpdateNote = Pick<Note, FieldNameNote.ownerId>
|
||||
export type TypeUpdateNote =
|
||||
| Pick<Note, FieldNameNote.ownerId>
|
||||
| Pick<Note, FieldNameNote.publiclyVisible>
|
||||
|
||||
@@ -421,7 +421,6 @@
|
||||
"owner": "Owner",
|
||||
"sharedWithUsers": "Shared with users",
|
||||
"sharedWithGroups": "Shared with groups",
|
||||
"sharedWithElse": "Shared with else...",
|
||||
"editUser": "Change {{name}}'s permissions to view and edit",
|
||||
"viewOnlyUser": "Change {{name}}'s permissions to view only",
|
||||
"removeUser": "Remove {{name}}'s permissions",
|
||||
@@ -439,7 +438,9 @@
|
||||
"placeholder": "Enter username of new note owner",
|
||||
"button": "Change the owner of this note"
|
||||
},
|
||||
"inconsistent": "This permission is overridden by another permission"
|
||||
"inconsistent": "This permission is overridden by another permission",
|
||||
"visibility": "Visibility",
|
||||
"publiclyVisible": "Show on the public explore page"
|
||||
},
|
||||
"shareLink": {
|
||||
"title": "Share link",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { DeleteApiRequestBuilder } from '../common/api-request-builder/delete-ap
|
||||
import { PutApiRequestBuilder } from '../common/api-request-builder/put-api-request-builder'
|
||||
import type {
|
||||
ChangeNoteOwnerInterface,
|
||||
ChangeNoteVisibilityInterface,
|
||||
NoteGroupPermissionUpdateInterface,
|
||||
NotePermissionsInterface,
|
||||
NoteUserPermissionUpdateInterface
|
||||
@@ -31,6 +32,24 @@ export const setNoteOwner = async (noteId: string, newOwner: string): Promise<No
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the visibility of a note by setting it as public or private.
|
||||
*
|
||||
* @param noteId - The unique identifier of the note to update.
|
||||
* @param publiclyVisible - A flag indicating whether the note should be publicly visible.
|
||||
* @returns A promise resolving to the updated permissions metadata for the note.
|
||||
*/
|
||||
export const setNotePublic = async (noteId: string, publiclyVisible: boolean): Promise<NotePermissionsInterface> => {
|
||||
const response = await new PutApiRequestBuilder<NotePermissionsInterface, ChangeNoteVisibilityInterface>(
|
||||
`notes/${noteId}/metadata/permissions/visibility`
|
||||
)
|
||||
.withJsonBody({
|
||||
publiclyVisible: publiclyVisible
|
||||
})
|
||||
.sendRequest()
|
||||
return response.asParsedJsonObject()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a permission for one user of a note.
|
||||
*
|
||||
|
||||
+2
@@ -12,6 +12,7 @@ import { PermissionSectionUsers } from './permission-section-users'
|
||||
import React from 'react'
|
||||
import { Modal } from 'react-bootstrap'
|
||||
import { cypressId } from '../../../../../../utils/cypress-attribute'
|
||||
import { PermissionSectionVisibility } from './permission-section-visibility'
|
||||
|
||||
/**
|
||||
* Modal for viewing and managing the permissions of the note.
|
||||
@@ -32,6 +33,7 @@ export const PermissionModal: React.FC<ModalVisibilityProps> = ({ show, onHide }
|
||||
<PermissionSectionOwner disabled={!isOwner} />
|
||||
<PermissionSectionUsers disabled={!isOwner} />
|
||||
<PermissionSectionSpecialGroups disabled={!isOwner} />
|
||||
<PermissionSectionVisibility disabled={!isOwner} />
|
||||
</Modal.Body>
|
||||
</CommonModal>
|
||||
)
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ export const PermissionSectionSpecialGroups: React.FC<PermissionDisabledProps> =
|
||||
return (
|
||||
<Fragment>
|
||||
<h5 className={'my-3'}>
|
||||
<Trans i18nKey={'editor.modal.permissions.sharedWithElse'} />
|
||||
<Trans i18nKey={'editor.modal.permissions.sharedWithGroups'} />
|
||||
</h5>
|
||||
<ul className={'list-group'}>
|
||||
<PermissionEntrySpecialGroup
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { setNotePublic } from '../../../../../../api/permissions'
|
||||
import { useApplicationState } from '../../../../../../hooks/common/use-application-state'
|
||||
import { setNotePermissionsFromServer } from '../../../../../../redux/note-details/methods'
|
||||
import { useUiNotifications } from '../../../../../notifications/ui-notification-boundary'
|
||||
import type { PermissionDisabledProps } from './permission-disabled.prop'
|
||||
import React, { type ChangeEvent, Fragment, useCallback } from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { Form } from 'react-bootstrap'
|
||||
|
||||
/**
|
||||
* Section in the permissions modal for managing whether the note should be visible on the explore page.
|
||||
*
|
||||
* @param disabled If the user is not the owner, functionality is disabled.
|
||||
*/
|
||||
export const PermissionSectionVisibility: React.FC<PermissionDisabledProps> = ({ disabled }) => {
|
||||
const noteAlias = useApplicationState((state) => state.noteDetails?.primaryAlias)
|
||||
const currentVisibility = useApplicationState((state) => state.noteDetails.permissions.publiclyVisible)
|
||||
const { showErrorNotification } = useUiNotifications()
|
||||
|
||||
const onSetChangeVisibility = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!noteAlias) {
|
||||
return
|
||||
}
|
||||
const newValue = event.target.checked
|
||||
setNotePublic(noteAlias, newValue)
|
||||
.then((updatedPermissions) => {
|
||||
setNotePermissionsFromServer(updatedPermissions)
|
||||
})
|
||||
.catch(showErrorNotification('editor.modal.permissions.error'))
|
||||
},
|
||||
[noteAlias, showErrorNotification]
|
||||
)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<h5 className={'my-3'}>
|
||||
<Trans i18nKey={'editor.modal.permissions.visibility'} />
|
||||
</h5>
|
||||
<ul className={'list-group'}>
|
||||
<li className={'list-group-item'}>
|
||||
<Form.Check
|
||||
disabled={disabled}
|
||||
reverse={true}
|
||||
type={'switch'}
|
||||
className={'d-flex flex-row align-items-center justify-content-between'}>
|
||||
<Form.Check.Label>
|
||||
<Trans i18nKey={'editor.modal.permissions.publiclyVisible'} />
|
||||
</Form.Check.Label>
|
||||
<Form.Check.Input disabled={disabled} onChange={onSetChangeVisibility} checked={currentVisibility} />
|
||||
</Form.Check>
|
||||
</li>
|
||||
</ul>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export const initialState: NoteDetails = {
|
||||
primaryAlias: '',
|
||||
permissions: {
|
||||
owner: null,
|
||||
publiclyVisible: false,
|
||||
sharedToGroups: [],
|
||||
sharedToUsers: []
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user