replace shared_access_token table with an extra column on access_token

This commit is contained in:
Hazelnoot 2025-06-21 13:11:57 -04:00
parent e5cf9d3f9a
commit 49dad22609
12 changed files with 111 additions and 147 deletions

View file

@ -5,10 +5,11 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AccessTokensRepository, SharedAccessTokensRepository } from '@/models/_.js';
import type { AccessTokensRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { CacheService } from '@/core/CacheService.js';
export const meta = {
requireCredential: true,
@ -85,10 +86,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.accessTokensRepository)
private accessTokensRepository: AccessTokensRepository,
@Inject(DI.sharedAccessTokensRepository)
private readonly sharedAccessTokenRepository: SharedAccessTokensRepository,
private readonly userEntityService: UserEntityService,
private readonly cacheService: CacheService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me, at) => {
@ -106,25 +105,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const tokens = await query.getMany();
return await Promise.all(tokens.map(async token => {
// TODO inline this table into a column w/ GIN index
const sharedTokens = await this.sharedAccessTokenRepository.find({
where: { accessTokenId: token.id },
relations: { grantee: true },
});
const users = await this.cacheService.getUsers(tokens.flatMap(token => token.granteeIds));
const packedUsers = await this.userEntityService.packMany(Array.from(users.values()), me, { token: at });
const packedUserMap = new Map(packedUsers.map(u => [u.id, u]));
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const grantees = await this.userEntityService.packMany(sharedTokens.map(t => t.grantee!), me, { token: at });
return {
id: token.id,
name: token.name ?? token.app?.name,
createdAt: this.idService.parse(token.id).date.toISOString(),
lastUsedAt: token.lastUsedAt?.toISOString(),
permission: token.app ? token.app.permission : token.permission,
rank: token.rank,
grantees,
};
return tokens.map(token => ({
id: token.id,
name: token.name ?? token.app?.name,
createdAt: this.idService.parse(token.id).date.toISOString(),
lastUsedAt: token.lastUsedAt?.toISOString(),
permission: token.app ? token.app.permission : token.permission,
rank: token.rank,
grantees: token.granteeIds
.map(id => packedUserMap.get(id))
.filter(user => user != null),
}));
});
}

View file

@ -58,6 +58,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
}
}
// TODO notify of access revoked
});
}
}

View file

@ -6,11 +6,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import type { SharedAccessTokensRepository } from '@/models/_.js';
import type { AccessTokensRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { Packed } from '@/misc/json-schema.js';
/* eslint-disable @typescript-eslint/no-non-null-assertion */
export const meta = {
requireCredential: true,
@ -65,25 +62,25 @@ export const paramDef = {} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.sharedAccessTokensRepository)
private readonly sharedAccessTokensRepository: SharedAccessTokensRepository,
@Inject(DI.accessTokensRepository)
private readonly accessTokensRepository: AccessTokensRepository,
private readonly userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, me, token) => {
const tokens = await this.sharedAccessTokensRepository.find({
where: { granteeId: me.id },
relations: { accessToken: true },
});
const tokens = await this.accessTokensRepository
.createQueryBuilder('token')
.where(':meIdAsList <@ token.granteeIds')
.getMany();
const users = tokens.map(token => token.accessToken!.userId);
const packedUsers: Packed<'UserLite'>[] = await this.userEntityService.packMany(users, me, { token });
const packedUserMap = new Map<string, Packed<'UserLite'>>(packedUsers.map(u => [u.id, u]));
const userIds = tokens.map(token => token.userId);
const packedUsers = await this.userEntityService.packMany(userIds, me, { token });
const packedUserMap = new Map(packedUsers.map(u => [u.id, u]));
return tokens.map(token => ({
id: token.accessTokenId,
permissions: token.accessToken!.permission,
user: packedUserMap.get(token.accessToken!.userId) as Packed<'UserLite'>,
id: token.id,
permissions: token.permission,
user: packedUserMap.get(token.userId),
}));
});
}

View file

@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import type { SharedAccessTokensRepository } from '@/models/_.js';
import type { AccessTokensRepository } from '@/models/_.js';
import { ApiError } from '@/server/api/error.js';
export const meta = {
@ -47,7 +47,7 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
grantId: { type: 'string' },
tokenId: { type: 'string' },
},
required: ['grantId'],
} as const;
@ -55,25 +55,25 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.sharedAccessTokensRepository)
private readonly sharedAccessTokensRepository: SharedAccessTokensRepository,
@Inject(DI.accessTokensRepository)
private readonly accessTokensRepository: AccessTokensRepository,
) {
super(meta, paramDef, async (ps, me) => {
const token = await this.sharedAccessTokensRepository.findOne({
where: { accessTokenId: ps.grantId, granteeId: me.id },
relations: { accessToken: true },
});
const token = await this.accessTokensRepository.findOneBy({ id: ps.tokenId });
if (!token) {
throw new ApiError(meta.errors.noSuchAccess);
}
return {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
token: token.accessToken!.token,
if (!token.granteeIds.includes(me.id)) {
throw new ApiError(meta.errors.noSuchAccess);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
userId: token.accessToken!.userId,
// TODO notify of login
return {
token: token.token,
userId: token.userId,
};
});
}

View file

@ -4,11 +4,10 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In } from 'typeorm';
import { SkSharedAccessToken } from '@/models/SkSharedAccessToken.js';
import { In } from 'typeorm';
import { ApiError } from '@/server/api/error.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AccessTokensRepository, SharedAccessTokensRepository, UsersRepository } from '@/models/_.js';
import type { AccessTokensRepository, UsersRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { NotificationService } from '@/core/NotificationService.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
@ -71,15 +70,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.accessTokensRepository)
private accessTokensRepository: AccessTokensRepository,
@Inject(DI.sharedAccessTokensRepository)
private readonly sharedAccessTokensRepository: SharedAccessTokensRepository,
@Inject(DI.usersRepository)
private readonly usersRepository: UsersRepository,
@Inject(DI.db)
private readonly db: DataSource,
private idService: IdService,
private notificationService: NotificationService,
) {
@ -97,27 +90,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const now = new Date();
const accessTokenId = this.idService.gen(now.getTime());
await this.db.transaction(async tem => {
// Insert access token doc
await this.accessTokensRepository.insert({
id: accessTokenId,
lastUsedAt: now,
session: ps.session,
userId: me.id,
token: accessToken,
hash: accessToken,
name: ps.name,
description: ps.description,
iconUrl: ps.iconUrl,
permission: ps.permission,
rank: ps.rank,
});
// Insert shared access grants
if (ps.grantees && ps.grantees.length > 0) {
const grants = ps.grantees.map(granteeId => new SkSharedAccessToken({ accessTokenId, granteeId }));
await this.sharedAccessTokensRepository.insert(grants);
}
// Insert access token doc
await this.accessTokensRepository.insert({
id: accessTokenId,
lastUsedAt: now,
session: ps.session,
userId: me.id,
token: accessToken,
hash: accessToken,
name: ps.name,
description: ps.description,
iconUrl: ps.iconUrl,
permission: ps.permission,
rank: ps.rank,
granteeIds: ps.grantees,
});
// TODO notify of access granted