mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
7ed41eadc6
* feat: add title and logoUrl to Share model Agent-Logs-Url: https://github.com/outline/outline/sessions/9bc9d438-6892-4903-9d32-6b6868f4fd97 Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * fix: use STRING(4096) for logoUrl column in migration Agent-Logs-Url: https://github.com/outline/outline/sessions/9bc9d438-6892-4903-9d32-6b6868f4fd97 Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * feat: use share title and logoUrl to override team branding on shared page Agent-Logs-Url: https://github.com/outline/outline/sessions/854d6d22-e80b-4673-b3b2-0f9cf43a3246 Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * refactor: use ShareValidation class constants for title/logoUrl max lengths Agent-Logs-Url: https://github.com/outline/outline/sessions/ea462d6a-d4d3-4882-ab8e-88060bf64877 Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * fix: use ShareValidation constants in @Length msg template literals Agent-Logs-Url: https://github.com/outline/outline/sessions/694116c2-47e8-4001-a103-c8a62c7ac71e Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> * feat: add display settings popover with custom title and icon for shares Move share toggles (search indexing, email subscriptions, show last modified, show TOC) into a popover triggered by a settings cog. The popover also includes inputs for a custom site title and icon upload to override team branding on shared pages. Rename logoUrl to iconUrl, loosen URL validation to allow relative attachment paths, and surface the popover in the shared page header for users with edit permission. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * styling * Display branding on single shared pages * Review comments * refactor * PR feedback * Lose 'Remove icon' button --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tommoor <380914+tommoor@users.noreply.github.com> Co-authored-by: Tom Moor <tom@getoutline.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
264 lines
5.3 KiB
TypeScript
264 lines
5.3 KiB
TypeScript
import type { InferAttributes, InferCreationAttributes } from "sequelize";
|
|
import { type SaveOptions } from "sequelize";
|
|
import {
|
|
ForeignKey,
|
|
BelongsTo,
|
|
Column,
|
|
DefaultScope,
|
|
Table,
|
|
Scopes,
|
|
DataType,
|
|
Default,
|
|
AllowNull,
|
|
Is,
|
|
Unique,
|
|
BeforeUpdate,
|
|
} from "sequelize-typescript";
|
|
import { UrlHelper } from "@shared/utils/UrlHelper";
|
|
import { ShareValidation } from "@shared/validations";
|
|
import env from "@server/env";
|
|
import { ValidationError } from "@server/errors";
|
|
import type { APIContext } from "@server/types";
|
|
import Collection from "./Collection";
|
|
import Document from "./Document";
|
|
import Team from "./Team";
|
|
import User from "./User";
|
|
import IdModel from "./base/IdModel";
|
|
import Fix from "./decorators/Fix";
|
|
import IsFQDN from "./validators/IsFQDN";
|
|
import IsUrlOrRelativePath from "./validators/IsUrlOrRelativePath";
|
|
import Length from "./validators/Length";
|
|
|
|
@DefaultScope(() => ({
|
|
include: [
|
|
{
|
|
association: "user",
|
|
paranoid: false,
|
|
},
|
|
{
|
|
association: "collection",
|
|
required: false,
|
|
},
|
|
{
|
|
association: "document",
|
|
required: false,
|
|
},
|
|
{
|
|
association: "team",
|
|
required: true,
|
|
},
|
|
],
|
|
}))
|
|
@Scopes(() => ({
|
|
withCollectionPermissions: (userId: string) => ({
|
|
include: [
|
|
{
|
|
attributes: [
|
|
"id",
|
|
"name",
|
|
"permission",
|
|
"sharing",
|
|
"urlId",
|
|
"teamId",
|
|
"deletedAt",
|
|
],
|
|
model: Collection.scope({
|
|
method: ["withMembership", userId],
|
|
}),
|
|
as: "collection",
|
|
},
|
|
{
|
|
model: Document.scope([
|
|
"withDrafts",
|
|
{
|
|
method: ["withMembership", userId],
|
|
},
|
|
]),
|
|
paranoid: true,
|
|
as: "document",
|
|
include: [
|
|
{
|
|
attributes: [
|
|
"id",
|
|
"name",
|
|
"permission",
|
|
"urlId",
|
|
"sharing",
|
|
"teamId",
|
|
"deletedAt",
|
|
],
|
|
model: Collection.scope({
|
|
method: ["withMembership", userId],
|
|
}),
|
|
as: "collection",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
association: "user",
|
|
paranoid: false,
|
|
},
|
|
{
|
|
association: "team",
|
|
},
|
|
],
|
|
}),
|
|
}))
|
|
@Table({ tableName: "shares", modelName: "share" })
|
|
@Fix
|
|
class Share extends IdModel<
|
|
InferAttributes<Share>,
|
|
Partial<InferCreationAttributes<Share>>
|
|
> {
|
|
@Column
|
|
published: boolean;
|
|
|
|
@Column
|
|
includeChildDocuments: boolean;
|
|
|
|
@Column
|
|
revokedAt: Date | null;
|
|
|
|
@Column
|
|
lastAccessedAt: Date | null;
|
|
|
|
/** Total count of times the shared link has been accessed */
|
|
@Default(0)
|
|
@Column
|
|
views: number;
|
|
|
|
@AllowNull
|
|
@Is({
|
|
args: UrlHelper.SHARE_URL_SLUG_REGEX,
|
|
msg: "Must be only alphanumeric and dashes",
|
|
})
|
|
@Column
|
|
urlId: string | null | undefined;
|
|
|
|
@Unique
|
|
@Length({ max: 255, msg: "domain must be 255 characters or less" })
|
|
@IsFQDN
|
|
@Column
|
|
domain: string | null;
|
|
|
|
@Default(false)
|
|
@Column
|
|
allowIndexing: boolean;
|
|
|
|
@Default(true)
|
|
@Column
|
|
allowSubscriptions: boolean;
|
|
|
|
@Default(false)
|
|
@Column
|
|
showLastUpdated: boolean;
|
|
|
|
@Default(false)
|
|
@Column
|
|
showTOC: boolean;
|
|
|
|
@AllowNull
|
|
@Length({
|
|
max: ShareValidation.maxTitleLength,
|
|
msg: `title must be ${ShareValidation.maxTitleLength} characters or less`,
|
|
})
|
|
@Column
|
|
title: string | null;
|
|
|
|
@AllowNull
|
|
@IsUrlOrRelativePath
|
|
@Length({
|
|
max: ShareValidation.maxIconUrlLength,
|
|
msg: `iconUrl must be ${ShareValidation.maxIconUrlLength} characters or less`,
|
|
})
|
|
@Column
|
|
iconUrl: string | null;
|
|
|
|
// hooks
|
|
|
|
@BeforeUpdate
|
|
static async checkDomain(model: Share, options: SaveOptions) {
|
|
if (!model.domain) {
|
|
return model;
|
|
}
|
|
|
|
model.domain = model.domain.toLowerCase();
|
|
|
|
const count = await Team.count({
|
|
...options,
|
|
where: {
|
|
domain: model.domain,
|
|
},
|
|
});
|
|
|
|
if (count > 0) {
|
|
throw ValidationError("Domain is already in use");
|
|
}
|
|
|
|
return model;
|
|
}
|
|
|
|
// getters
|
|
|
|
get isRevoked() {
|
|
return !!this.revokedAt;
|
|
}
|
|
|
|
get canonicalUrl() {
|
|
if (this.domain) {
|
|
const url = new URL(env.URL);
|
|
return `${url.protocol}//${this.domain}${url.port ? `:${url.port}` : ""}`;
|
|
}
|
|
|
|
return this.urlId
|
|
? `${this.team.url}/s/${this.urlId}`
|
|
: `${this.team.url}/s/${this.id}`;
|
|
}
|
|
|
|
// associations
|
|
|
|
@BelongsTo(() => User, "revokedById")
|
|
revokedBy: User;
|
|
|
|
@ForeignKey(() => User)
|
|
@Column(DataType.UUID)
|
|
revokedById: string;
|
|
|
|
@BelongsTo(() => User, "userId")
|
|
user: User;
|
|
|
|
@ForeignKey(() => User)
|
|
@Column(DataType.UUID)
|
|
userId: string;
|
|
|
|
@BelongsTo(() => Team, "teamId")
|
|
team: Team;
|
|
|
|
@ForeignKey(() => Team)
|
|
@Column(DataType.UUID)
|
|
teamId: string;
|
|
|
|
@BelongsTo(() => Collection, "collectionId")
|
|
collection: Collection | null;
|
|
|
|
@ForeignKey(() => Collection)
|
|
@Column(DataType.UUID)
|
|
collectionId: string | null;
|
|
|
|
@BelongsTo(() => Document, "documentId")
|
|
document: Document | null;
|
|
|
|
@ForeignKey(() => Document)
|
|
@Column(DataType.UUID)
|
|
documentId: string | null;
|
|
|
|
revoke(ctx: APIContext) {
|
|
const { user } = ctx.state.auth;
|
|
this.revokedAt = new Date();
|
|
this.revokedById = user.id;
|
|
return this.saveWithCtx(ctx, undefined, { name: "revoke" });
|
|
}
|
|
}
|
|
|
|
export default Share;
|