Skip to content

Commit

Permalink
feat(server): support selfhost licenses (#8947)
Browse files Browse the repository at this point in the history
  • Loading branch information
forehalo committed Jan 22, 2025
1 parent 22e424d commit 994d758
Show file tree
Hide file tree
Showing 31 changed files with 1,653 additions and 127 deletions.
1 change: 0 additions & 1 deletion .docker/dev/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
DATABASE_LOCATION=./postgres
DB_PASSWORD=affine
DB_USERNAME=affine
DB_DATABASE_NAME=affine
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- CreateTable
CREATE TABLE "licenses" (
"key" VARCHAR NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"revealed_at" TIMESTAMPTZ(3),
"installed_at" TIMESTAMPTZ(3),
"validate_key" VARCHAR,

CONSTRAINT "licenses_pkey" PRIMARY KEY ("key")
);

-- CreateTable
CREATE TABLE "installed_licenses" (
"key" VARCHAR NOT NULL,
"workspace_id" VARCHAR NOT NULL,
"quantity" INTEGER NOT NULL DEFAULT 1,
"recurring" VARCHAR NOT NULL,
"installed_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"validate_key" VARCHAR NOT NULL,
"validated_at" TIMESTAMPTZ(3) NOT NULL,
"expired_at" TIMESTAMPTZ(3),

CONSTRAINT "installed_licenses_pkey" PRIMARY KEY ("key")
);

-- CreateIndex
CREATE UNIQUE INDEX "installed_licenses_workspace_id_key" ON "installed_licenses"("workspace_id");
2 changes: 1 addition & 1 deletion packages/backend/server/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
provider = "postgresql"
34 changes: 29 additions & 5 deletions packages/backend/server/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -569,15 +569,39 @@ model Invoice {
@@index([targetId])
@@map("invoices")
}

model License {
key String @id @map("key") @db.VarChar
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
revealedAt DateTime? @map("revealed_at") @db.Timestamptz(3)
installedAt DateTime? @map("installed_at") @db.Timestamptz(3)
validateKey String? @map("validate_key") @db.VarChar
@@map("licenses")
}

model InstalledLicense {
key String @id @map("key") @db.VarChar
workspaceId String @unique @map("workspace_id") @db.VarChar
quantity Int @default(1) @db.Integer
recurring String @db.VarChar
installedAt DateTime @default(now()) @map("installed_at") @db.Timestamptz(3)
validateKey String @map("validate_key") @db.VarChar
validatedAt DateTime @map("validated_at") @db.Timestamptz(3)
expiredAt DateTime? @map("expired_at") @db.Timestamptz(3)
@@map("installed_licenses")
}

// Blob table only exists for fast non-data queries.
// like, total size of blobs in a workspace, or blob list for sync service.
// it should only be a map of metadata of blobs stored anywhere else
model Blob {
workspaceId String @map("workspace_id") @db.VarChar
key String @db.VarChar
size Int @db.Integer
mime String @db.VarChar
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
workspaceId String @map("workspace_id") @db.VarChar
key String @db.VarChar
size Int @db.Integer
mime String @db.VarChar
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(3)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1635,6 +1635,87 @@ Generated by [AVA](https://avajs.dev).
<!--/$-->␊
`

> Your AFFiNE Self-Hosted Team Workspace license is ready
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
<!--$-->␊
<table␊
align="center"␊
width="100%"␊
border="0"␊
cellpadding="0"␊
cellspacing="0"␊
role="presentation"␊
>␊
<tbody>␊
<tr>␊
<td>␊
<p␊
style="font-size:20px;line-height:28px;margin:24px 0 0;font-weight:600;font-family:Inter, Arial, Helvetica, sans-serif;color:#141414"␊
>␊
Here is your license key.␊
</p>␊
</td>␊
</tr>␊
</tbody>␊
</table>␊
<table␊
align="center"␊
width="100%"␊
border="0"␊
cellpadding="0"␊
cellspacing="0"␊
role="presentation"␊
>␊
<tbody>␊
<tr>␊
<td>␊
<table␊
align="center"␊
width="100%"␊
border="0"␊
cellpadding="0"␊
cellspacing="0"␊
role="presentation"␊
>␊
<tbody style="width:100%">␊
<tr style="width:100%">␊
<pre␊
style="font-size:15px;font-weight:400;line-height:24px;font-family:Inter, Arial, Helvetica, sans-serif;margin:24px 0 0;color:#141414;white-space:nowrap;border:1px solid rgba(0,0,0,.1);padding:8px 10px;border-radius:4px;background-color:#F5F5F5"␊
>␊
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</pre␊
>␊
</tr>␊
</tbody>␊
</table>␊
<table␊
align="center"␊
width="100%"␊
border="0"␊
cellpadding="0"␊
cellspacing="0"␊
role="presentation"␊
>␊
<tbody style="width:100%">␊
<tr style="width:100%">␊
<p␊
style="font-size:15px;line-height:24px;margin:24px 0 0;font-weight:400;font-family:Inter, Arial, Helvetica, sans-serif;color:#141414"␊
>␊
You can use this key to upgrade your selfhost workspace in<!-- -->␊
<span style="font-weight:600"␊
>Settings &gt; Workspace &gt; License</span␊
>.␊
</p>␊
</tr>␊
</tbody>␊
</table>␊
</td>␊
</tr>␊
</tbody>␊
</table>␊
<!--/$-->␊
`

> Your workspace Test Workspace has been deleted
`<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">␊
Expand Down
Binary file not shown.
7 changes: 5 additions & 2 deletions packages/backend/server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { UserModule } from './core/user';
import { WorkspaceModule } from './core/workspaces';
import { ModelsModule } from './models';
import { REGISTERED_PLUGINS } from './plugins';
import { LicenseModule } from './plugins/license';
import { ENABLED_PLUGINS } from './plugins/registry';

export const FunctionalityModules = [
Expand Down Expand Up @@ -203,7 +204,8 @@ export function buildAppModule() {
GqlModule,
StorageModule,
ServerConfigModule,
WorkspaceModule
WorkspaceModule,
LicenseModule
)

// self hosted server only
Expand All @@ -214,7 +216,8 @@ export function buildAppModule() {
ENABLED_PLUGINS.forEach(name => {
const plugin = REGISTERED_PLUGINS.get(name);
if (!plugin) {
throw new Error(`Unknown plugin ${name}`);
new Logger('AppBuilder').warn(`Unknown plugin ${name}`);
return;
}

factor.use(plugin);
Expand Down
34 changes: 34 additions & 0 deletions packages/backend/server/src/base/error/def.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,4 +607,38 @@ export const USER_FRIENDLY_ERRORS = {
type: 'bad_request',
message: 'Captcha verification failed.',
},

// license errors
invalid_license_session_id: {
type: 'invalid_input',
message: 'Invalid session id to generate license key.',
},
license_revealed: {
type: 'action_forbidden',
message:
'License key has been revealed. Please check your mail box of the one provided during checkout.',
},
workspace_license_already_exists: {
type: 'action_forbidden',
message: 'Workspace already has a license applied.',
},
license_not_found: {
type: 'resource_not_found',
message: 'License not found.',
},
invalid_license_to_activate: {
type: 'bad_request',
message: 'Invalid license to activate.',
},
invalid_license_update_params: {
type: 'invalid_input',
args: { reason: 'string' },
message: ({ reason }) => `Invalid license update params. ${reason}`,
},
workspace_members_exceed_limit_to_downgrade: {
type: 'bad_request',
args: { limit: 'number' },
message: ({ limit }) =>
`You cannot downgrade the workspace from team workspace because there are more than ${limit} members that are currently active.`,
},
} satisfies Record<string, UserFriendlyErrorOptions>;
61 changes: 59 additions & 2 deletions packages/backend/server/src/base/error/errors.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,56 @@ export class CaptchaVerificationFailed extends UserFriendlyError {
super('bad_request', 'captcha_verification_failed', message);
}
}

export class InvalidLicenseSessionId extends UserFriendlyError {
constructor(message?: string) {
super('invalid_input', 'invalid_license_session_id', message);
}
}

export class LicenseRevealed extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'license_revealed', message);
}
}

export class WorkspaceLicenseAlreadyExists extends UserFriendlyError {
constructor(message?: string) {
super('action_forbidden', 'workspace_license_already_exists', message);
}
}

export class LicenseNotFound extends UserFriendlyError {
constructor(message?: string) {
super('resource_not_found', 'license_not_found', message);
}
}

export class InvalidLicenseToActivate extends UserFriendlyError {
constructor(message?: string) {
super('bad_request', 'invalid_license_to_activate', message);
}
}
@ObjectType()
class InvalidLicenseUpdateParamsDataType {
@Field() reason!: string
}

export class InvalidLicenseUpdateParams extends UserFriendlyError {
constructor(args: InvalidLicenseUpdateParamsDataType, message?: string | ((args: InvalidLicenseUpdateParamsDataType) => string)) {
super('invalid_input', 'invalid_license_update_params', message, args);
}
}
@ObjectType()
class WorkspaceMembersExceedLimitToDowngradeDataType {
@Field() limit!: number
}

export class WorkspaceMembersExceedLimitToDowngrade extends UserFriendlyError {
constructor(args: WorkspaceMembersExceedLimitToDowngradeDataType, message?: string | ((args: WorkspaceMembersExceedLimitToDowngradeDataType) => string)) {
super('bad_request', 'workspace_members_exceed_limit_to_downgrade', message, args);
}
}
export enum ErrorNames {
INTERNAL_SERVER_ERROR,
TOO_MANY_REQUEST,
Expand Down Expand Up @@ -669,7 +719,14 @@ export enum ErrorNames {
MAILER_SERVICE_IS_NOT_CONFIGURED,
CANNOT_DELETE_ALL_ADMIN_ACCOUNT,
CANNOT_DELETE_OWN_ACCOUNT,
CAPTCHA_VERIFICATION_FAILED
CAPTCHA_VERIFICATION_FAILED,
INVALID_LICENSE_SESSION_ID,
LICENSE_REVEALED,
WORKSPACE_LICENSE_ALREADY_EXISTS,
LICENSE_NOT_FOUND,
INVALID_LICENSE_TO_ACTIVATE,
INVALID_LICENSE_UPDATE_PARAMS,
WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE
}
registerEnumType(ErrorNames, {
name: 'ErrorNames'
Expand All @@ -678,5 +735,5 @@ registerEnumType(ErrorNames, {
export const ErrorDataUnionType = createUnionType({
name: 'ErrorDataUnion',
types: () =>
[WrongSignInCredentialsDataType, UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, DocNotFoundDataType, DocAccessDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType] as const,
[WrongSignInCredentialsDataType, UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, DocNotFoundDataType, DocAccessDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseUpdateParamsDataType, WorkspaceMembersExceedLimitToDowngradeDataType] as const,
});
18 changes: 18 additions & 0 deletions packages/backend/server/src/base/helpers/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,24 @@ export class URLHelper {
return new URLSearchParams(query).toString();
}

addSimpleQuery(
url: string,
key: string,
value: string | number | boolean,
escape = true
) {
const urlObj = new URL(url);
if (escape) {
urlObj.searchParams.set(key, encodeURIComponent(value));
return urlObj.toString();
} else {
const query =
(urlObj.search ? urlObj.search + '&' : '?') + `${key}=${value}`;

return urlObj.origin + urlObj.pathname + query;
}
}

url(path: string, query: Record<string, any> = {}) {
const url = new URL(path, this.origin);

Expand Down
2 changes: 2 additions & 0 deletions packages/backend/server/src/base/mailer/mail.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
renderTeamBecomeCollaboratorMail,
renderTeamDeleteIn1MonthMail,
renderTeamDeleteIn24HoursMail,
renderTeamLicenseMail,
renderTeamWorkspaceDeletedMail,
renderTeamWorkspaceExpiredMail,
renderTeamWorkspaceExpireSoonMail,
Expand Down Expand Up @@ -188,4 +189,5 @@ export class MailService {
renderTeamWorkspaceExpireSoonMail
);
sendTeamExpiredMail = this.makeWorkspace(renderTeamWorkspaceExpiredMail);
sendTeamLicenseMail = this.make(renderTeamLicenseMail);
}
Loading

0 comments on commit 994d758

Please sign in to comment.