feat(server): support access token (#13372)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced user access tokens, enabling users to generate, list, and
revoke personal access tokens via the GraphQL API.
* Added GraphQL mutations and queries for managing access tokens,
including token creation (with optional expiration), listing, and
revocation.
* Implemented authentication support for private API endpoints using
access tokens in addition to session cookies.

* **Bug Fixes**
  * None.

* **Tests**
* Added comprehensive tests for access token creation, listing,
revocation, expiration handling, and authentication using tokens.

* **Chores**
* Updated backend models, schema, and database migrations to support
access token functionality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Yii
2025-07-31 13:55:10 +08:00
committed by GitHub
parent feb42e34be
commit 49e8f339d4
21 changed files with 564 additions and 9 deletions
@@ -96,6 +96,21 @@ test('should be able to visit private api if signed in', async t => {
t.is(res.body.user.id, u1.id);
});
test('should be able to visit private api with access token', async t => {
const models = t.context.app.get(Models);
const token = await models.accessToken.create({
userId: u1.id,
name: 'test',
});
const res = await request(server)
.get('/private')
.set('Authorization', `Bearer ${token.token}`)
.expect(HttpStatus.OK);
t.is(res.body.user.id, u1.id);
});
test('should be able to parse session cookie', async t => {
const spy = Sinon.spy(auth, 'getUserSession');
await request(server)
@@ -0,0 +1,27 @@
import { faker } from '@faker-js/faker';
import type { AccessToken } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { Mocker } from './factory';
export type MockAccessTokenInput = Omit<
Prisma.AccessTokenUncheckedCreateInput,
'token'
>;
export type MockedAccessToken = AccessToken;
export class MockAccessToken extends Mocker<
MockAccessTokenInput,
MockedAccessToken
> {
override async create(input: MockAccessTokenInput) {
return await this.db.accessToken.create({
data: {
...input,
name: input.name ?? faker.lorem.word(),
token: 'ut_' + faker.string.hexadecimal({ length: 37 }),
},
});
}
}
@@ -4,6 +4,7 @@ export * from './user.mock';
export * from './workspace.mock';
export * from './workspace-user.mock';
import { MockAccessToken } from './access-token.mock';
import { MockCopilotProvider } from './copilot.mock';
import { MockDocMeta } from './doc-meta.mock';
import { MockDocSnapshot } from './doc-snapshot.mock';
@@ -26,6 +27,7 @@ export const Mockers = {
DocMeta: MockDocMeta,
DocSnapshot: MockDocSnapshot,
DocUser: MockDocUser,
AccessToken: MockAccessToken,
};
export { MockCopilotProvider, MockEventBus, MockJobQueue, MockMailer };