replace shared_access_token table with an extra column on access_token
This commit is contained in:
parent
e5cf9d3f9a
commit
49dad22609
12 changed files with 111 additions and 147 deletions
|
|
@ -96,6 +96,5 @@ export const DI = {
|
|||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
||||
noteScheduleRepository: Symbol('noteScheduleRepository'),
|
||||
sharedAccessTokensRepository: Symbol('sharedAccessTokensRepository'),
|
||||
//#endregion
|
||||
};
|
||||
|
|
|
|||
|
|
@ -97,4 +97,18 @@ export class MiAccessToken {
|
|||
comment: 'Limits the user\' rank (user, moderator, or admin) when using this token. If null (default), then uses the user\'s actual rank.',
|
||||
})
|
||||
public rank: AccessTokenRank | null;
|
||||
|
||||
@Index('IDX_access_token_granteeIds', { synchronize: false })
|
||||
@Column({
|
||||
...id(),
|
||||
array: true, default: '{}',
|
||||
comment: 'IDs of other users who are permitted to access and use this token.',
|
||||
})
|
||||
public granteeIds: string[];
|
||||
|
||||
public constructor(props?: Partial<MiAccessToken>) {
|
||||
if (props) {
|
||||
Object.assign(this, props);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,6 @@ import {
|
|||
SkApContext,
|
||||
SkApFetchLog,
|
||||
SkApInboxLog,
|
||||
SkSharedAccessToken,
|
||||
} from './_.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DataSource } from 'typeorm';
|
||||
|
|
@ -153,12 +152,6 @@ const $apInboxLogsRepository: Provider = {
|
|||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $skSharedAccessToken: Provider = {
|
||||
provide: DI.sharedAccessTokensRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(SkSharedAccessToken).extend(miRepository as MiRepository<SkSharedAccessToken>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $noteFavoritesRepository: Provider = {
|
||||
provide: DI.noteFavoritesRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository<MiNoteFavorite>),
|
||||
|
|
@ -592,7 +585,6 @@ const $noteScheduleRepository: Provider = {
|
|||
$apContextRepository,
|
||||
$apFetchLogsRepository,
|
||||
$apInboxLogsRepository,
|
||||
$skSharedAccessToken,
|
||||
$noteFavoritesRepository,
|
||||
$noteThreadMutingsRepository,
|
||||
$noteReactionsRepository,
|
||||
|
|
@ -675,7 +667,6 @@ const $noteScheduleRepository: Provider = {
|
|||
$apContextRepository,
|
||||
$apFetchLogsRepository,
|
||||
$apInboxLogsRepository,
|
||||
$skSharedAccessToken,
|
||||
$noteFavoritesRepository,
|
||||
$noteThreadMutingsRepository,
|
||||
$noteReactionsRepository,
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
import { id } from '@/models/util/id.js';
|
||||
import { MiUser } from '@/models/User.js';
|
||||
import { MiAccessToken } from '@/models/AccessToken.js';
|
||||
|
||||
@Entity('shared_access_token')
|
||||
export class SkSharedAccessToken {
|
||||
@PrimaryColumn({
|
||||
...id(),
|
||||
comment: 'ID of the access token that is shared',
|
||||
})
|
||||
public accessTokenId: string;
|
||||
|
||||
@ManyToOne(() => MiAccessToken, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({
|
||||
name: 'accessTokenId',
|
||||
referencedColumnName: 'id',
|
||||
foreignKeyConstraintName: 'FK_shared_access_token_accessTokenId',
|
||||
})
|
||||
public accessToken: MiAccessToken;
|
||||
|
||||
@Index('IDX_shared_access_token_granteeId')
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'ID of the user who is allowed to use this access token',
|
||||
})
|
||||
public granteeId: string;
|
||||
|
||||
@ManyToOne(() => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({
|
||||
name: 'granteeId',
|
||||
referencedColumnName: 'id',
|
||||
foreignKeyConstraintName: 'FK_shared_access_token_granteeId',
|
||||
})
|
||||
public grantee?: MiUser;
|
||||
|
||||
constructor(props?: Partial<SkSharedAccessToken>) {
|
||||
if (props) {
|
||||
Object.assign(this, props);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -98,7 +98,6 @@ import { SkApInboxLog } from '@/models/SkApInboxLog.js';
|
|||
import { SkApFetchLog } from '@/models/SkApFetchLog.js';
|
||||
import { SkApContext } from '@/models/SkApContext.js';
|
||||
import { SkLatestNote } from '@/models/LatestNote.js';
|
||||
import { SkSharedAccessToken } from '@/models/SkSharedAccessToken.js';
|
||||
|
||||
export interface MiRepository<T extends ObjectLiteral> {
|
||||
createTableColumnNames(this: Repository<T> & MiRepository<T>): string[];
|
||||
|
|
@ -169,7 +168,6 @@ export {
|
|||
SkApContext,
|
||||
SkApFetchLog,
|
||||
SkApInboxLog,
|
||||
SkSharedAccessToken,
|
||||
MiAbuseUserReport,
|
||||
MiAbuseReportNotificationRecipient,
|
||||
MiAccessToken,
|
||||
|
|
@ -330,4 +328,3 @@ export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRep
|
|||
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;
|
||||
export type NoteEditRepository = Repository<NoteEdit> & MiRepository<NoteEdit>;
|
||||
export type NoteScheduleRepository = Repository<MiNoteSchedule> & MiRepository<MiNoteSchedule>;
|
||||
export type SharedAccessTokensRepository = Repository<SkSharedAccessToken> & MiRepository<SkSharedAccessToken>;
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ import { SkLatestNote } from '@/models/LatestNote.js';
|
|||
import { SkApContext } from '@/models/SkApContext.js';
|
||||
import { SkApFetchLog } from '@/models/SkApFetchLog.js';
|
||||
import { SkApInboxLog } from '@/models/SkApInboxLog.js';
|
||||
import { SkSharedAccessToken } from '@/models/SkSharedAccessToken.js';
|
||||
|
||||
pg.types.setTypeParser(20, Number);
|
||||
|
||||
|
|
@ -214,7 +213,6 @@ export const entities = [
|
|||
SkApContext,
|
||||
SkApFetchLog,
|
||||
SkApInboxLog,
|
||||
SkSharedAccessToken,
|
||||
MiAnnouncement,
|
||||
MiAnnouncementRead,
|
||||
MiMeta,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO notify of access revoked
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue