mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-15 00:56:26 +08:00
feat(server): support installable license (#12181)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added support for installing self-hosted team licenses via encrypted license files. - Introduced a new "Onetime" license variant for self-hosted environments. - Added a GraphQL mutation to upload and install license files. - License details now display the license variant. - **Bug Fixes** - Improved error messages for license activation and expiration, including dynamic reasons. - **Localization** - Updated and improved license-related error messages for better clarity. - **Tests** - Added comprehensive end-to-end tests for license installation scenarios. - **Chores** - Enhanced environment variable handling and public key management for license verification. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -77,11 +77,24 @@ export class TestingApp extends NestApplication {
|
||||
assert(init.body, 'body is required for gql request');
|
||||
assert(init.headers, 'headers is required for gql request');
|
||||
|
||||
const res = await this.request('post', '/graphql')
|
||||
.send(init?.body)
|
||||
const req = this.request('post', '/graphql')
|
||||
.set('accept', 'application/json')
|
||||
.set(init.headers as Record<string, string>);
|
||||
|
||||
if (init.body instanceof FormData) {
|
||||
for (const [key, value] of init.body.entries()) {
|
||||
if (value instanceof File) {
|
||||
req.attach(key, Buffer.from(await value.arrayBuffer()));
|
||||
} else {
|
||||
req.field(key, value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
req.send(init.body);
|
||||
}
|
||||
|
||||
const res = await req;
|
||||
|
||||
return new Response(Buffer.from(JSON.stringify(res.body)), {
|
||||
status: res.status,
|
||||
headers: res.headers,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,144 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { installLicenseMutation, SubscriptionVariant } from '@affine/graphql';
|
||||
|
||||
import { Workspace, WorkspaceRole } from '../../../models';
|
||||
import {
|
||||
createApp,
|
||||
e2e,
|
||||
MockedUser,
|
||||
Mockers,
|
||||
refreshEnv,
|
||||
type TestingApp,
|
||||
} from '../test';
|
||||
|
||||
const testWorkspaceId = 'd6f52bc7-d62a-4822-804a-335fa7dfe5a6';
|
||||
const testPublicKey = `-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEkXqe9HR32rSgaNxaePXbwHxoanUq
|
||||
1DcQTV1twn+G47/HiY+rj7oOw3cLNzOVe7+4Uxn8SMZ/XLImtqFQ6I4WVg==
|
||||
-----END PUBLIC KEY-----`;
|
||||
|
||||
const fixturesDir = join(import.meta.dirname, '__fixtures__');
|
||||
function getLicense(file: string) {
|
||||
return new File([readFileSync(join(fixturesDir, file))], 'test-license.lic', {
|
||||
type: 'application/octet-stream',
|
||||
});
|
||||
}
|
||||
|
||||
const licenses = {
|
||||
valid: getLicense('valid.license'),
|
||||
expired: getLicense('expired.license'),
|
||||
expiredEndAt: getLicense('expired-end-at.license'),
|
||||
};
|
||||
|
||||
let app: TestingApp;
|
||||
let workspace: Workspace;
|
||||
let owner: MockedUser;
|
||||
|
||||
e2e.before(async () => {
|
||||
process.env.DEPLOYMENT_TYPE = 'selfhosted';
|
||||
process.env.AFFiNE_PRO_PUBLIC_KEY = testPublicKey;
|
||||
refreshEnv();
|
||||
|
||||
app = await createApp();
|
||||
await app.models.workspace.delete(testWorkspaceId);
|
||||
owner = await app.signup();
|
||||
workspace = await app.create(Mockers.Workspace, {
|
||||
id: testWorkspaceId,
|
||||
owner,
|
||||
});
|
||||
});
|
||||
|
||||
e2e.beforeEach(async () => {
|
||||
await app.login(owner);
|
||||
});
|
||||
|
||||
e2e.after.always(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
e2e('should install file license', async t => {
|
||||
const res = await app.gql({
|
||||
query: installLicenseMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
license: licenses.valid,
|
||||
},
|
||||
});
|
||||
|
||||
t.is(res.installLicense.variant, SubscriptionVariant.Onetime);
|
||||
});
|
||||
|
||||
e2e('should not allow to install license if not owner', async t => {
|
||||
const user = await app.signup();
|
||||
await app.create(Mockers.WorkspaceUser, {
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
type: WorkspaceRole.Collaborator,
|
||||
});
|
||||
|
||||
await t.throwsAsync(
|
||||
app.gql({
|
||||
query: installLicenseMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
license: licenses.valid,
|
||||
},
|
||||
}),
|
||||
{
|
||||
message: `You do not have permission to access Space ${workspace.id}.`,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
e2e(`should not install other workspace's license file`, async t => {
|
||||
const owner = await app.signup();
|
||||
const workspace = await app.create(Mockers.Workspace, {
|
||||
owner,
|
||||
});
|
||||
|
||||
await t.throwsAsync(
|
||||
app.gql({
|
||||
query: installLicenseMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
license: licenses.valid,
|
||||
},
|
||||
}),
|
||||
{
|
||||
message:
|
||||
'Invalid license to activate. Workspace mismatched with license.',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
e2e('should not install expired license', async t => {
|
||||
await t.throwsAsync(
|
||||
app.gql({
|
||||
query: installLicenseMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
license: licenses.expired,
|
||||
},
|
||||
}),
|
||||
{
|
||||
message: 'License has expired.',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
e2e('should not install license with expired end date', async t => {
|
||||
await t.throwsAsync(
|
||||
app.gql({
|
||||
query: installLicenseMutation,
|
||||
variables: {
|
||||
workspaceId: workspace.id,
|
||||
license: licenses.expiredEndAt,
|
||||
},
|
||||
}),
|
||||
{
|
||||
message: 'License has expired.',
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import test, { registerCompletionHandler } from 'ava';
|
||||
|
||||
import { Env } from '../../env';
|
||||
import { type TestingApp } from './create-app';
|
||||
|
||||
export const e2e = test;
|
||||
@@ -9,3 +10,11 @@ export const app: TestingApp = globalThis.app;
|
||||
registerCompletionHandler(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
export function refreshEnv() {
|
||||
globalThis.env = new Env();
|
||||
}
|
||||
|
||||
export * from '../mocks';
|
||||
export { createApp } from './create-app';
|
||||
export type { TestingApp };
|
||||
|
||||
Reference in New Issue
Block a user