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
@@ -0,0 +1,82 @@
import test from 'ava';
import { createModule } from '../../__tests__/create-module';
import { Mockers } from '../../__tests__/mocks';
import { Due } from '../../base';
import { Models } from '..';
const module = await createModule();
const models = module.get(Models);
test.after.always(async () => {
await module.close();
});
test('should create access token', async t => {
const user = await module.create(Mockers.User);
const token = await models.accessToken.create({
userId: user.id,
name: 'test',
});
t.is(token.userId, user.id);
t.is(token.name, 'test');
t.truthy(token.token);
t.truthy(token.createdAt);
t.is(token.expiresAt, null);
});
test('should create access token with expiration', async t => {
const user = await module.create(Mockers.User);
const token = await models.accessToken.create({
userId: user.id,
name: 'test',
expiresAt: Due.after('30d'),
});
t.truthy(token.expiresAt);
t.truthy(token.expiresAt! > new Date());
});
test('should list access tokens without token value', async t => {
const user = await module.create(Mockers.User);
await module.create(Mockers.AccessToken, { userId: user.id }, 3);
const listed = await models.accessToken.list(user.id);
t.is(listed.length, 3);
// @ts-expect-error not exists
t.is(listed[0].token, undefined);
});
test('should be able to revoke access token', async t => {
const user = await module.create(Mockers.User);
const token = await module.create(Mockers.AccessToken, { userId: user.id });
await models.accessToken.revoke(token.id, user.id);
const listed = await models.accessToken.list(user.id);
t.is(listed.length, 0);
});
test('should be able to get access token by token value', async t => {
const user = await module.create(Mockers.User);
const token = await module.create(Mockers.AccessToken, { userId: user.id });
const found = await models.accessToken.getByToken(token.token);
t.is(found?.id, token.id);
t.is(found?.userId, user.id);
t.is(found?.name, token.name);
});
test('should not get expired access token', async t => {
const user = await module.create(Mockers.User);
const token = await module.create(Mockers.AccessToken, {
userId: user.id,
expiresAt: Due.before('1s'),
});
const found = await models.accessToken.getByToken(token.token);
t.is(found, null);
});