basic support for Shared Access Accounts
This commit is contained in:
parent
7715f36a99
commit
fa5a46f379
22 changed files with 789 additions and 32 deletions
|
|
@ -298,6 +298,8 @@ export * as 'i/registry/scopes-with-domain' from './endpoints/i/registry/scopes-
|
|||
export * as 'i/registry/set' from './endpoints/i/registry/set.js';
|
||||
export * as 'i/revoke-token' from './endpoints/i/revoke-token.js';
|
||||
export * as 'i/signin-history' from './endpoints/i/signin-history.js';
|
||||
export * as 'i/shared-access/list' from './endpoints/i/shared-access/list.js';
|
||||
export * as 'i/shared-access/login' from './endpoints/i/shared-access/login.js';
|
||||
export * as 'i/unpin' from './endpoints/i/unpin.js';
|
||||
export * as 'i/update' from './endpoints/i/update.js';
|
||||
export * as 'i/update-email' from './endpoints/i/update-email.js';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
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 { 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,
|
||||
secure: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user: {
|
||||
ref: 'UserLite',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
permissions: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
token: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 2 calls per second
|
||||
limit: {
|
||||
duration: 1000,
|
||||
max: 2,
|
||||
},
|
||||
} as const;
|
||||
|
||||
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.sharedAccessToken)
|
||||
private readonly sharedAccessTokensRepository: SharedAccessTokensRepository,
|
||||
|
||||
private readonly userEntityService: UserEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const tokens = await this.sharedAccessTokensRepository.find({
|
||||
where: { granteeId: me.id },
|
||||
relations: { accessToken: true },
|
||||
});
|
||||
|
||||
const users = tokens.map(token => token.accessToken!.userId);
|
||||
const packedUsers: Packed<'UserLite'>[] = await this.userEntityService.packMany(users, me);
|
||||
const packedUserMap = new Map<string, Packed<'UserLite'>>(packedUsers.map(u => [u.id, u]));
|
||||
|
||||
return await Promise.all(tokens.map(async token => ({
|
||||
id: token.accessTokenId,
|
||||
permissions: token.accessToken!.permission,
|
||||
user: packedUserMap.get(token.accessToken!.userId) as Packed<'UserLite'>,
|
||||
})));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
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 { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
secure: true,
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
token: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchAccess: {
|
||||
message: 'No such access',
|
||||
code: 'NO_SUCH_ACCESS',
|
||||
id: 'd536e0f2-47fc-4d66-843c-f9276e98030f',
|
||||
httpStatusCode: 403,
|
||||
},
|
||||
},
|
||||
|
||||
// 2 calls per second
|
||||
limit: {
|
||||
duration: 1000,
|
||||
max: 2,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
grantId: { type: 'string' },
|
||||
},
|
||||
required: ['grantId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.sharedAccessToken)
|
||||
private readonly sharedAccessTokensRepository: SharedAccessTokensRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const token = await this.sharedAccessTokensRepository.findOne({
|
||||
where: { accessTokenId: ps.grantId, granteeId: me.id },
|
||||
relations: { accessToken: true },
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
throw new ApiError(meta.errors.noSuchAccess);
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
token: token.accessToken!.token,
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
userId: token.accessToken!.userId,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,11 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In } from 'typeorm';
|
||||
import { SkSharedAccessToken } from '@/models/SkSharedAccessToken.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { AccessTokensRepository } from '@/models/_.js';
|
||||
import type { AccessTokensRepository, SharedAccessTokensRepository, UsersRepository } from '@/models/_.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
|
|
@ -29,6 +32,14 @@ export const meta = {
|
|||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: 'a89abd3d-f0bc-4cce-beb1-2f446f4f1e6a',
|
||||
},
|
||||
},
|
||||
|
||||
// 10 calls per 5 seconds
|
||||
limit: {
|
||||
duration: 1000 * 5,
|
||||
|
|
@ -46,6 +57,9 @@ export const paramDef = {
|
|||
permission: { type: 'array', uniqueItems: true, items: {
|
||||
type: 'string',
|
||||
} },
|
||||
grantees: { type: 'array', uniqueItems: true, items: {
|
||||
type: 'string',
|
||||
} },
|
||||
},
|
||||
required: ['session', 'permission'],
|
||||
} as const;
|
||||
|
|
@ -56,29 +70,56 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
@Inject(DI.accessTokensRepository)
|
||||
private accessTokensRepository: AccessTokensRepository,
|
||||
|
||||
@Inject(DI.sharedAccessToken)
|
||||
private readonly sharedAccessTokensRepository: SharedAccessTokensRepository,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private readonly usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.db)
|
||||
private readonly db: DataSource,
|
||||
|
||||
private idService: IdService,
|
||||
private notificationService: NotificationService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.grantees && ps.grantees.length > 0) {
|
||||
const existingCount = await this.usersRepository.countBy({ id: In(ps.grantees) });
|
||||
if (existingCount !== ps.grantees.length) {
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate access token
|
||||
const accessToken = secureRndstr(32);
|
||||
|
||||
const now = new Date();
|
||||
const accessTokenId = this.idService.gen(now.getTime());
|
||||
|
||||
// Insert access token doc
|
||||
await this.accessTokensRepository.insert({
|
||||
id: this.idService.gen(now.getTime()),
|
||||
lastUsedAt: now,
|
||||
session: ps.session,
|
||||
userId: me.id,
|
||||
token: accessToken,
|
||||
hash: accessToken,
|
||||
name: ps.name,
|
||||
description: ps.description,
|
||||
iconUrl: ps.iconUrl,
|
||||
permission: ps.permission,
|
||||
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,
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO notify of access granted
|
||||
|
||||
// アクセストークンが生成されたことを通知
|
||||
this.notificationService.createNotification(me.id, 'createToken', {});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue