forked from FoundKeyGang/FoundKey
Compare commits
46 commits
3900d2f716
...
1b3d89c40d
Author | SHA1 | Date | |
---|---|---|---|
1b3d89c40d | |||
55d8d5a626 | |||
e89c0135bd | |||
eea2eb4919 | |||
114d416de0 | |||
c2372315f7 | |||
09bc3cf95a | |||
de3cdb5833 | |||
a732cdc1ad | |||
a8f82050c8 | |||
8e12b9a33e | |||
6583d0c43d | |||
c02a03168d | |||
85419326f8 | |||
eaa11647f0 | |||
61a2db49df | |||
79ddbafd0d | |||
0e1459e5cf | |||
e8e82dac82 | |||
9690244848 | |||
4db25e4b1f | |||
549302e9c0 | |||
a3354904af | |||
28f65bebfc | |||
2204adc657 | |||
b11e4053db | |||
e2ef800708 | |||
a7048f17f7 | |||
ddf3e2c3db | |||
52afff800a | |||
33f0b24c56 | |||
7685b92511 | |||
8276bd3bdc | |||
aed2752470 | |||
4a3b91d658 | |||
9317d25078 | |||
fc36bb8880 | |||
711bb8be7d | |||
275136cf8b | |||
aa33708b90 | |||
d75e295ee8 | |||
766ab1c4c4 | |||
99c459a21a | |||
bd68096ea9 | |||
c411669133 | |||
639fa74d43 |
82 changed files with 637 additions and 432 deletions
|
@ -1098,6 +1098,7 @@ _permissions:
|
||||||
"write:notes": "Create and delete notes"
|
"write:notes": "Create and delete notes"
|
||||||
"read:notifications": "Read notifications"
|
"read:notifications": "Read notifications"
|
||||||
"write:notifications": "Mark notifications as read and create custom notifications"
|
"write:notifications": "Mark notifications as read and create custom notifications"
|
||||||
|
"read:reactions": "View reactions"
|
||||||
"write:reactions": "Create and delete reactions"
|
"write:reactions": "Create and delete reactions"
|
||||||
"write:votes": "Vote in polls"
|
"write:votes": "Vote in polls"
|
||||||
"read:pages": "List and read pages"
|
"read:pages": "List and read pages"
|
||||||
|
|
|
@ -35,7 +35,7 @@ export async function getHtml(url: string, accept = 'text/html, */*', timeout =
|
||||||
return await res.text();
|
return await res.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getResponse(args: { url: string, method: string, body?: string, headers: Record<string, string>, timeout?: number, size?: number }) {
|
export async function getResponse(args: { url: string, method: string, body?: string, headers: Record<string, string>, timeout?: number, size?: number, redirect: 'follow' | 'manual' | 'error' = 'follow' }) {
|
||||||
const timeout = args.timeout || 10 * SECOND;
|
const timeout = args.timeout || 10 * SECOND;
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
@ -47,8 +47,9 @@ export async function getResponse(args: { url: string, method: string, body?: st
|
||||||
method: args.method,
|
method: args.method,
|
||||||
headers: args.headers,
|
headers: args.headers,
|
||||||
body: args.body,
|
body: args.body,
|
||||||
|
redirect: args.redirect,
|
||||||
timeout,
|
timeout,
|
||||||
size: args.size || 10 * 1024 * 1024,
|
size: args.size || 10 * 1024 * 1024, // 10 MiB
|
||||||
agent: getAgentByUrl,
|
agent: getAgentByUrl,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
10
packages/backend/src/misc/password.ts
Normal file
10
packages/backend/src/misc/password.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
|
export async function hashPassword(password: string): Promise<string> {
|
||||||
|
const salt = await bcrypt.genSalt(8);
|
||||||
|
return await bcrypt.hash(password, salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function comparePassword(password: string, hash: string): Promise<boolean> {
|
||||||
|
return await bcrypt.compare(password, hash);
|
||||||
|
}
|
|
@ -6,11 +6,10 @@ import { Meta } from '@/models/entities/meta.js';
|
||||||
* Returns whether a specific host (punycoded) should be blocked.
|
* Returns whether a specific host (punycoded) should be blocked.
|
||||||
*
|
*
|
||||||
* @param host punycoded instance host
|
* @param host punycoded instance host
|
||||||
* @param meta a Promise contatining the information from the meta table (optional)
|
* @param meta a resolved Meta table
|
||||||
* @returns whether the given host should be blocked
|
* @returns whether the given host should be blocked
|
||||||
*/
|
*/
|
||||||
|
export async function shouldBlockInstance(host: Instance['host'], meta?: Meta): Promise<boolean> {
|
||||||
export async function shouldBlockInstance(host: Instance['host'], meta: Promise<Meta> = fetchMeta()): Promise<boolean> {
|
const { blockedHosts } = meta ?? await fetchMeta();
|
||||||
const { blockedHosts } = await meta;
|
|
||||||
return blockedHosts.some(blockedHost => host === blockedHost || host.endsWith('.' + blockedHost));
|
return blockedHosts.some(blockedHost => host === blockedHost || host.endsWith('.' + blockedHost));
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,8 @@ const deadThreshold = 7 * DAY;
|
||||||
* @returns array of punycoded instance hosts that should be skipped (subset of hosts parameter)
|
* @returns array of punycoded instance hosts that should be skipped (subset of hosts parameter)
|
||||||
*/
|
*/
|
||||||
export async function skippedInstances(hosts: Array<Instance['host']>): Promise<Array<Instance['host']>> {
|
export async function skippedInstances(hosts: Array<Instance['host']>): Promise<Array<Instance['host']>> {
|
||||||
// Resolve the boolean promises before filtering
|
// first check for blocked instances since that info may already be in memory
|
||||||
const meta = fetchMeta();
|
const meta = await fetchMeta();
|
||||||
const shouldSkip = await Promise.all(hosts.map(host => shouldBlockInstance(host, meta)));
|
const shouldSkip = await Promise.all(hosts.map(host => shouldBlockInstance(host, meta)));
|
||||||
const skipped = hosts.filter((_, i) => shouldSkip[i]);
|
const skipped = hosts.filter((_, i) => shouldSkip[i]);
|
||||||
|
|
||||||
|
|
|
@ -61,50 +61,24 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
||||||
return thumbnail ? (file.thumbnailUrl || (isImage ? (file.webpublicUrl || file.url) : null)) : (file.webpublicUrl || file.url);
|
return thumbnail ? (file.thumbnailUrl || (isImage ? (file.webpublicUrl || file.url) : null)) : (file.webpublicUrl || file.url);
|
||||||
},
|
},
|
||||||
|
|
||||||
async calcDriveUsageOf(user: User['id'] | { id: User['id'] }): Promise<number> {
|
calcDriveUsageOf(id: User['id']): Promise<number> {
|
||||||
const id = typeof user === 'object' ? user.id : user;
|
return db.query('SELECT SUM(size) AS sum FROM drive_file WHERE "userId" = $1 AND NOT "isLink"', [id])
|
||||||
|
.then(res => res[0].sum as number ?? 0);
|
||||||
const { sum } = await this
|
|
||||||
.createQueryBuilder('file')
|
|
||||||
.where('file.userId = :id', { id })
|
|
||||||
.andWhere('file.isLink = FALSE')
|
|
||||||
.select('SUM(file.size)', 'sum')
|
|
||||||
.getRawOne();
|
|
||||||
|
|
||||||
return parseInt(sum, 10) || 0;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async calcDriveUsageOfHost(host: string): Promise<number> {
|
calcDriveUsageOfHost(host: string): Promise<number> {
|
||||||
const { sum } = await this
|
return db.query('SELECT SUM(size) AS sum FROM drive_file WHERE "userHost" = $1 AND NOT "isLink"', [toPuny(host)])
|
||||||
.createQueryBuilder('file')
|
.then(res => res[0].sum as number ?? 0);
|
||||||
.where('file.userHost = :host', { host: toPuny(host) })
|
|
||||||
.andWhere('file.isLink = FALSE')
|
|
||||||
.select('SUM(file.size)', 'sum')
|
|
||||||
.getRawOne();
|
|
||||||
|
|
||||||
return parseInt(sum, 10) || 0;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async calcDriveUsageOfLocal(): Promise<number> {
|
calcDriveUsageOfLocal(): Promise<number> {
|
||||||
const { sum } = await this
|
return db.query('SELECT SUM(size) AS sum FROM drive_file WHERE "userHost" IS NULL AND NOT "isLink"')
|
||||||
.createQueryBuilder('file')
|
.then(res => res[0].sum as number ?? 0);
|
||||||
.where('file.userHost IS NULL')
|
|
||||||
.andWhere('file.isLink = FALSE')
|
|
||||||
.select('SUM(file.size)', 'sum')
|
|
||||||
.getRawOne();
|
|
||||||
|
|
||||||
return parseInt(sum, 10) || 0;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async calcDriveUsageOfRemote(): Promise<number> {
|
calcDriveUsageOfRemote(): Promise<number> {
|
||||||
const { sum } = await this
|
return db.query('SELECT SUM(size) AS sum FROM drive_file WHERE "userHost" IS NOT NULL AND NOT "isLink"')
|
||||||
.createQueryBuilder('file')
|
.then(res => res[0].sum as number ?? 0);
|
||||||
.where('file.userHost IS NOT NULL')
|
|
||||||
.andWhere('file.isLink = FALSE')
|
|
||||||
.select('SUM(file.size)', 'sum')
|
|
||||||
.getRawOne();
|
|
||||||
|
|
||||||
return parseInt(sum, 10) || 0;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async pack(
|
async pack(
|
||||||
|
@ -152,26 +126,7 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
||||||
const file = typeof src === 'object' ? src : await this.findOneBy({ id: src });
|
const file = typeof src === 'object' ? src : await this.findOneBy({ id: src });
|
||||||
if (file == null) return null;
|
if (file == null) return null;
|
||||||
|
|
||||||
return await awaitAll<Packed<'DriveFile'>>({
|
return await this.pack(file);
|
||||||
id: file.id,
|
|
||||||
createdAt: file.createdAt.toISOString(),
|
|
||||||
name: file.name,
|
|
||||||
type: file.type,
|
|
||||||
md5: file.md5,
|
|
||||||
size: file.size,
|
|
||||||
isSensitive: file.isSensitive,
|
|
||||||
blurhash: file.blurhash,
|
|
||||||
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
|
||||||
url: opts.self ? file.url : this.getPublicUrl(file, false),
|
|
||||||
thumbnailUrl: this.getPublicUrl(file, true),
|
|
||||||
comment: file.comment,
|
|
||||||
folderId: file.folderId,
|
|
||||||
folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
|
|
||||||
detail: true,
|
|
||||||
}) : null,
|
|
||||||
userId: opts.withUser ? file.userId : null,
|
|
||||||
user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async packMany(
|
async packMany(
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import Bull from 'bull';
|
import Bull from 'bull';
|
||||||
import { In, LessThan } from 'typeorm';
|
import { In, LessThan } from 'typeorm';
|
||||||
import { AttestationChallenges, AuthSessions, Mutings, PasswordResetRequests, Signins } from '@/models/index.js';
|
import { AttestationChallenges, AuthSessions, Mutings, Notifications, PasswordResetRequests, Signins } from '@/models/index.js';
|
||||||
import { publishUserEvent } from '@/services/stream.js';
|
import { publishUserEvent } from '@/services/stream.js';
|
||||||
import { MINUTE, DAY } from '@/const.js';
|
import { MINUTE, DAY, MONTH } from '@/const.js';
|
||||||
import { queueLogger } from '@/queue/logger.js';
|
import { queueLogger } from '@/queue/logger.js';
|
||||||
|
|
||||||
const logger = queueLogger.createSubLogger('check-expired');
|
const logger = queueLogger.createSubLogger('check-expired');
|
||||||
|
@ -26,22 +26,30 @@ export async function checkExpired(job: Bull.Job<Record<string, unknown>>, done:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OlderThan = (millis: number) => {
|
||||||
|
return LessThan(new Date(new Date().getTime() - millis));
|
||||||
|
};
|
||||||
|
|
||||||
await Signins.delete({
|
await Signins.delete({
|
||||||
// 60 days, or roughly equal to two months
|
createdAt: OlderThan(2 * MONTH),
|
||||||
createdAt: LessThan(new Date(new Date().getTime() - 60 * DAY)),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await AttestationChallenges.delete({
|
await AttestationChallenges.delete({
|
||||||
createdAt: LessThan(new Date(new Date().getTime() - 5 * MINUTE)),
|
createdAt: OlderThan(5 * MINUTE),
|
||||||
});
|
});
|
||||||
|
|
||||||
await PasswordResetRequests.delete({
|
await PasswordResetRequests.delete({
|
||||||
// this timing should be the same as in @/server/api/endpoints/reset-password.ts
|
// this timing should be the same as in @/server/api/endpoints/reset-password.ts
|
||||||
createdAt: LessThan(new Date(new Date().getTime() - 30 * MINUTE)),
|
createdAt: OlderThan(30 * MINUTE),
|
||||||
});
|
});
|
||||||
|
|
||||||
await AuthSessions.delete({
|
await AuthSessions.delete({
|
||||||
createdAt: LessThan(new Date(new Date().getTime() - 15 * MINUTE)),
|
createdAt: OlderThan(15 * MINUTE),
|
||||||
|
});
|
||||||
|
|
||||||
|
await Notifications.delete({
|
||||||
|
isRead: true,
|
||||||
|
createdAt: OlderThan(3 * MONTH),
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.succ('Deleted expired data.');
|
logger.succ('Deleted expired data.');
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||||
import accept from '@/services/following/requests/accept.js';
|
import { acceptFollowRequest } from '@/services/following/requests/accept.js';
|
||||||
import { relayAccepted } from '@/services/relay.js';
|
import { relayAccepted } from '@/services/relay.js';
|
||||||
import { IFollow } from '@/remote/activitypub/type.js';
|
import { IFollow } from '@/remote/activitypub/type.js';
|
||||||
import { DbResolver } from '@/remote/activitypub/db-resolver.js';
|
import { DbResolver } from '@/remote/activitypub/db-resolver.js';
|
||||||
|
@ -24,6 +24,6 @@ export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<st
|
||||||
return await relayAccepted(match[1]);
|
return await relayAccepted(match[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await accept(actor, follower);
|
await acceptFollowRequest(actor, follower);
|
||||||
return 'ok';
|
return 'ok';
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||||
import { toArray } from '@/prelude/array.js';
|
import { toArray } from '@/prelude/array.js';
|
||||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||||
|
import { extractDbHost } from '@/misc/convert-host.js';
|
||||||
|
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
||||||
import { apLogger } from '../logger.js';
|
import { apLogger } from '../logger.js';
|
||||||
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag } from '../type.js';
|
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag, getApId } from '../type.js';
|
||||||
import create from './create/index.js';
|
import create from './create/index.js';
|
||||||
import performDeleteActivity from './delete/index.js';
|
import performDeleteActivity from './delete/index.js';
|
||||||
import performUpdateActivity from './update/index.js';
|
import performUpdateActivity from './update/index.js';
|
||||||
|
@ -18,7 +20,7 @@ import remove from './remove/index.js';
|
||||||
import block from './block/index.js';
|
import block from './block/index.js';
|
||||||
import flag from './flag/index.js';
|
import flag from './flag/index.js';
|
||||||
|
|
||||||
export async function performActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver) {
|
export async function performActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
|
||||||
if (isCollectionOrOrderedCollection(activity)) {
|
if (isCollectionOrOrderedCollection(activity)) {
|
||||||
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
|
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
|
||||||
const act = await resolver.resolve(item);
|
const act = await resolver.resolve(item);
|
||||||
|
@ -38,6 +40,11 @@ export async function performActivity(actor: CacheableRemoteUser, activity: IObj
|
||||||
async function performOneActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
|
async function performOneActivity(actor: CacheableRemoteUser, activity: IObject, resolver: Resolver): Promise<void> {
|
||||||
if (actor.isSuspended) return;
|
if (actor.isSuspended) return;
|
||||||
|
|
||||||
|
if (typeof activity.id !== 'undefined') {
|
||||||
|
const host = extractDbHost(getApId(activity));
|
||||||
|
if (await shouldBlockInstance(host)) return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isCreate(activity)) {
|
if (isCreate(activity)) {
|
||||||
await create(actor, activity, resolver);
|
await create(actor, activity, resolver);
|
||||||
} else if (isDelete(activity)) {
|
} else if (isDelete(activity)) {
|
||||||
|
@ -55,7 +62,7 @@ async function performOneActivity(actor: CacheableRemoteUser, activity: IObject,
|
||||||
} else if (isAdd(activity)) {
|
} else if (isAdd(activity)) {
|
||||||
await add(actor, activity, resolver).catch(err => apLogger.error(err));
|
await add(actor, activity, resolver).catch(err => apLogger.error(err));
|
||||||
} else if (isRemove(activity)) {
|
} else if (isRemove(activity)) {
|
||||||
await remove(actor, activity).catch(err => apLogger.error(err));
|
await remove(actor, activity, resolver).catch(err => apLogger.error(err));
|
||||||
} else if (isAnnounce(activity)) {
|
} else if (isAnnounce(activity)) {
|
||||||
await announce(actor, activity, resolver);
|
await announce(actor, activity, resolver);
|
||||||
} else if (isLike(activity)) {
|
} else if (isLike(activity)) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import unfollow from '@/services/following/delete.js';
|
import unfollow from '@/services/following/delete.js';
|
||||||
import cancelRequest from '@/services/following/requests/cancel.js';
|
import { cancelFollowRequest } from '@/services/following/requests/cancel.js';
|
||||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||||
import { FollowRequests, Followings } from '@/models/index.js';
|
import { FollowRequests, Followings } from '@/models/index.js';
|
||||||
import { IFollow } from '@/remote/activitypub/type.js';
|
import { IFollow } from '@/remote/activitypub/type.js';
|
||||||
|
@ -29,7 +29,7 @@ export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<st
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (req) {
|
if (req) {
|
||||||
await cancelRequest(followee, actor);
|
await cancelFollowRequest(followee, actor);
|
||||||
return 'ok: follow request canceled';
|
return 'ok: follow request canceled';
|
||||||
} else if (following) {
|
} else if (following) {
|
||||||
await unfollow(actor, followee);
|
await unfollow(actor, followee);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { URL } from 'node:url';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import { getUserKeypair } from '@/misc/keypair-store.js';
|
import { getUserKeypair } from '@/misc/keypair-store.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
|
@ -26,6 +27,8 @@ export async function request(user: { id: User['id'] }, url: string, object: any
|
||||||
method: req.request.method,
|
method: req.request.method,
|
||||||
headers: req.request.headers,
|
headers: req.request.headers,
|
||||||
body,
|
body,
|
||||||
|
// don't allow redirects on the inbox
|
||||||
|
redirect: 'error',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -37,22 +40,33 @@ export async function request(user: { id: User['id'] }, url: string, object: any
|
||||||
export async function signedGet(url: string, user: { id: User['id'] }): Promise<any> {
|
export async function signedGet(url: string, user: { id: User['id'] }): Promise<any> {
|
||||||
const keypair = await getUserKeypair(user.id);
|
const keypair = await getUserKeypair(user.id);
|
||||||
|
|
||||||
const req = createSignedGet({
|
for (let redirects = 0; redirects < 3; redirects++) {
|
||||||
key: {
|
const req = createSignedGet({
|
||||||
privateKeyPem: keypair.privateKey,
|
key: {
|
||||||
keyId: `${config.url}/users/${user.id}#main-key`,
|
privateKeyPem: keypair.privateKey,
|
||||||
},
|
keyId: `${config.url}/users/${user.id}#main-key`,
|
||||||
url,
|
},
|
||||||
additionalHeaders: {
|
url,
|
||||||
'User-Agent': config.userAgent,
|
additionalHeaders: {
|
||||||
},
|
'User-Agent': config.userAgent,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const res = await getResponse({
|
const res = await getResponse({
|
||||||
url,
|
url,
|
||||||
method: req.request.method,
|
method: req.request.method,
|
||||||
headers: req.request.headers,
|
headers: req.request.headers,
|
||||||
});
|
redirect: 'manual',
|
||||||
|
});
|
||||||
|
|
||||||
return await res.json();
|
if (res.status >= 300 && res.status < 400) {
|
||||||
|
// Have been redirected, need to make a new signature.
|
||||||
|
// Use Location header and fetched URL as the base URL.
|
||||||
|
url = new URL(res.headers.get('Location'), url).href;
|
||||||
|
} else {
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('too many redirects');
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,9 +34,7 @@ export class Resolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async resolveCollection(value: string | IObject): Promise<ICollection | IOrderedCollection> {
|
public async resolveCollection(value: string | IObject): Promise<ICollection | IOrderedCollection> {
|
||||||
const collection = typeof value === 'string'
|
const collection = await this.resolve(value);
|
||||||
? await this.resolve(value)
|
|
||||||
: value;
|
|
||||||
|
|
||||||
if (isCollectionOrOrderedCollection(collection)) {
|
if (isCollectionOrOrderedCollection(collection)) {
|
||||||
return collection;
|
return collection;
|
||||||
|
@ -45,12 +43,18 @@ export class Resolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async resolve(value: string | IObject): Promise<IObject> {
|
public async resolve(value?: string | IObject | null, allowRedirect = false): Promise<IObject> {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
throw new Error('resolvee is null (or undefined)');
|
throw new Error('resolvee is null (or undefined)');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
|
if (typeof value.id !== 'undefined') {
|
||||||
|
const host = extractDbHost(getApId(value));
|
||||||
|
if (await shouldBlockInstance(host)) {
|
||||||
|
throw new Error('instance is blocked');
|
||||||
|
}
|
||||||
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +79,7 @@ export class Resolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await shouldBlockInstance(host)) {
|
if (await shouldBlockInstance(host)) {
|
||||||
throw new Error('Instance is blocked');
|
throw new Error('instance is blocked');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.user) {
|
if (!this.user) {
|
||||||
|
@ -94,7 +98,7 @@ export class Resolver {
|
||||||
)
|
)
|
||||||
// Did we actually get the object that corresponds to the canonical URL?
|
// Did we actually get the object that corresponds to the canonical URL?
|
||||||
// Does the host we requested stuff from actually correspond to the host that owns the activity?
|
// Does the host we requested stuff from actually correspond to the host that owns the activity?
|
||||||
|| !(getApId(object) == null || getApId(object) === value)
|
|| !(getApId(object) == null || getApId(object) === value || allowRedirect)
|
||||||
) {
|
) {
|
||||||
throw new Error('invalid response');
|
throw new Error('invalid response');
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,28 +5,24 @@ import authenticate, { AuthenticationError } from './authenticate.js';
|
||||||
import call from './call.js';
|
import call from './call.js';
|
||||||
import { ApiError } from './error.js';
|
import { ApiError } from './error.js';
|
||||||
|
|
||||||
export async function handler(endpoint: IEndpoint, ctx: Koa.Context): Promise<void> {
|
function getRequestArguments(ctx: Koa.Context): Record<string, any> {
|
||||||
const body = ctx.is('multipart/form-data')
|
const args = {
|
||||||
? (ctx.request as any).body
|
...(ctx.params || {}),
|
||||||
: ctx.method === 'GET'
|
...ctx.query,
|
||||||
? ctx.query
|
...(ctx.request.body || {}),
|
||||||
: ctx.request.body;
|
|
||||||
|
|
||||||
const error = (e: ApiError): void => {
|
|
||||||
ctx.status = e.httpStatusCode;
|
|
||||||
if (e.httpStatusCode === 401) {
|
|
||||||
ctx.response.set('WWW-Authenticate', 'Bearer');
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
error: {
|
|
||||||
message: e!.message,
|
|
||||||
code: e!.code,
|
|
||||||
...(e!.info ? { info: e!.info } : {}),
|
|
||||||
endpoint: endpoint.name,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// For security reasons, we drop the i parameter if it's a GET request
|
||||||
|
if (ctx.method === 'GET') {
|
||||||
|
delete args['i'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handler(endpoint: IEndpoint, ctx: Koa.Context): Promise<void> {
|
||||||
|
const body = getRequestArguments(ctx);
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
// for GET requests, do not even pass on the body parameter as it is considered unsafe
|
// for GET requests, do not even pass on the body parameter as it is considered unsafe
|
||||||
await authenticate(ctx.headers.authorization, ctx.method === 'GET' ? null : body['i']).then(async ([user, app]) => {
|
await authenticate(ctx.headers.authorization, ctx.method === 'GET' ? null : body['i']).then(async ([user, app]) => {
|
||||||
|
@ -43,13 +39,13 @@ export async function handler(endpoint: IEndpoint, ctx: Koa.Context): Promise<vo
|
||||||
ctx.body = typeof res === 'string' ? JSON.stringify(res) : res;
|
ctx.body = typeof res === 'string' ? JSON.stringify(res) : res;
|
||||||
}
|
}
|
||||||
}).catch((e: ApiError) => {
|
}).catch((e: ApiError) => {
|
||||||
error(e);
|
e.apply(ctx, endpoint.name);
|
||||||
});
|
});
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
if (e instanceof AuthenticationError) {
|
if (e instanceof AuthenticationError) {
|
||||||
error(new ApiError('AUTHENTICATION_FAILED', e.message));
|
new ApiError('AUTHENTICATION_FAILED', e.message).apply(ctx, endpoint.name);
|
||||||
} else {
|
} else {
|
||||||
error(new ApiError());
|
new ApiError().apply(ctx, endpoint.name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { generateKeyPair } from 'node:crypto';
|
import { generateKeyPair } from 'node:crypto';
|
||||||
import bcrypt from 'bcryptjs';
|
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
import { Users, UsedUsernames } from '@/models/index.js';
|
import { Users, UsedUsernames } from '@/models/index.js';
|
||||||
import { UserProfile } from '@/models/entities/user-profile.js';
|
import { UserProfile } from '@/models/entities/user-profile.js';
|
||||||
import { genId } from '@/misc/gen-id.js';
|
import { genId } from '@/misc/gen-id.js';
|
||||||
import { toPunyNullable } from '@/misc/convert-host.js';
|
import { toPunyNullable } from '@/misc/convert-host.js';
|
||||||
|
import { hashPassword } from '@/misc/password.js';
|
||||||
import { UserKeypair } from '@/models/entities/user-keypair.js';
|
import { UserKeypair } from '@/models/entities/user-keypair.js';
|
||||||
import { usersChart } from '@/services/chart/index.js';
|
import { usersChart } from '@/services/chart/index.js';
|
||||||
import { UsedUsername } from '@/models/entities/used-username.js';
|
import { UsedUsername } from '@/models/entities/used-username.js';
|
||||||
|
@ -33,9 +33,7 @@ export async function signup(opts: {
|
||||||
throw new ApiError('INVALID_PASSWORD');
|
throw new ApiError('INVALID_PASSWORD');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate hash of password
|
hash = await hashPassword(password);
|
||||||
const salt = await bcrypt.genSalt(8);
|
|
||||||
hash = await bcrypt.hash(password, salt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate secret
|
// Generate secret
|
||||||
|
|
|
@ -702,6 +702,24 @@ export interface IEndpointMeta {
|
||||||
* 正常応答をキャッシュ (Cache-Control: public) する秒数
|
* 正常応答をキャッシュ (Cache-Control: public) する秒数
|
||||||
*/
|
*/
|
||||||
readonly cacheSec?: number;
|
readonly cacheSec?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API v2 options
|
||||||
|
*/
|
||||||
|
readonly v2?: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP verb this endpoint supports
|
||||||
|
*/
|
||||||
|
readonly method: 'get' | 'put' | 'post' | 'patch' | 'delete';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path alias for v2 endpoint
|
||||||
|
*
|
||||||
|
* @example (v0) /api/notes/create -> /api/v2/notes
|
||||||
|
*/
|
||||||
|
readonly alias?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEndpoint {
|
export interface IEndpoint {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import { Users } from '@/models/index.js';
|
import { Users } from '@/models/index.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
import { signup } from '../../../common/signup.js';
|
import { signup } from '../../../common/signup.js';
|
||||||
|
|
||||||
|
@ -17,6 +18,8 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
errors: ['ACCESS_DENIED'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -31,10 +34,17 @@ export const paramDef = {
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, _me) => {
|
export default define(meta, paramDef, async (ps, _me) => {
|
||||||
const me = _me ? await Users.findOneByOrFail({ id: _me.id }) : null;
|
const me = _me ? await Users.findOneByOrFail({ id: _me.id }) : null;
|
||||||
const noUsers = (await Users.countBy({
|
if (me == null) {
|
||||||
host: IsNull(),
|
// check if this is the initial setup
|
||||||
})) === 0;
|
const noUsers = (await Users.countBy({
|
||||||
if (!noUsers && !me?.isAdmin) throw new Error('access denied');
|
host: IsNull(),
|
||||||
|
})) === 0;
|
||||||
|
if (!noUsers) {
|
||||||
|
throw new ApiError('ACCESS_DENIED');
|
||||||
|
}
|
||||||
|
} else if (!me.isAdmin) {
|
||||||
|
throw new ApiError('ACCESS_DENIED');
|
||||||
|
}
|
||||||
|
|
||||||
const { account, secret } = await signup({
|
const { account, secret } = await signup({
|
||||||
username: ps.username,
|
username: ps.username,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Users } from '@/models/index.js';
|
import { Users } from '@/models/index.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { doPostSuspend } from '@/services/suspend-user.js';
|
import { doPostSuspend } from '@/services/suspend-user.js';
|
||||||
import { publishUserEvent } from '@/services/stream.js';
|
import { publishUserEvent } from '@/services/stream.js';
|
||||||
import { createDeleteAccountJob } from '@/queue/index.js';
|
import { createDeleteAccountJob } from '@/queue/index.js';
|
||||||
|
@ -9,6 +10,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
|
|
||||||
|
errors: ['NO_SUCH_USER', 'IS_ADMIN', 'IS_MODERATOR'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -24,15 +27,11 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
const user = await Users.findOneBy({ id: ps.userId });
|
const user = await Users.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new Error('user not found');
|
throw new ApiError('NO_SUCH_USER');
|
||||||
}
|
} else if (user.isAdmin) {
|
||||||
|
throw new ApiError('IS_ADMIN');
|
||||||
if (user.isAdmin) {
|
} else if(user.isModerator) {
|
||||||
throw new Error('cannot suspend admin');
|
throw new ApiError('IS_MODERATOR');
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isModerator) {
|
|
||||||
throw new Error('cannot suspend moderator');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Users.isLocalUser(user)) {
|
if (Users.isLocalUser(user)) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Instances } from '@/models/index.js';
|
import { Instances } from '@/models/index.js';
|
||||||
import { toPuny } from '@/misc/convert-host.js';
|
import { toPuny } from '@/misc/convert-host.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
|
|
||||||
|
@ -8,6 +9,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
|
|
||||||
|
errors: ['NO_SUCH_OBJECT'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -23,7 +26,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
const instance = await Instances.findOneBy({ host: toPuny(ps.host) });
|
const instance = await Instances.findOneBy({ host: toPuny(ps.host) });
|
||||||
|
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
throw new Error('instance not found');
|
throw new ApiError('NO_SUCH_OBJECT');
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchInstanceMetadata(instance, true);
|
fetchInstanceMetadata(instance, true);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Instances } from '@/models/index.js';
|
import { Instances } from '@/models/index.js';
|
||||||
import { toPuny } from '@/misc/convert-host.js';
|
import { toPuny } from '@/misc/convert-host.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -7,6 +8,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
|
|
||||||
|
errors: ['NO_SUCH_OBJECT'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -23,7 +26,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
const instance = await Instances.findOneBy({ host: toPuny(ps.host) });
|
const instance = await Instances.findOneBy({ host: toPuny(ps.host) });
|
||||||
|
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
throw new Error('instance not found');
|
throw new ApiError('NO_SUCH_OBJECT');
|
||||||
}
|
}
|
||||||
|
|
||||||
Instances.update({ host: toPuny(ps.host) }, {
|
Instances.update({ host: toPuny(ps.host) }, {
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import { Users } from '@/models/index.js';
|
import { Users } from '@/models/index.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { publishInternalEvent } from '@/services/stream.js';
|
import { publishInternalEvent } from '@/services/stream.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['admin'],
|
tags: ['admin'],
|
||||||
|
|
||||||
|
description: 'Grants a user moderator privileges. Administrators cannot be granted moderator privileges.',
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireAdmin: true,
|
requireAdmin: true,
|
||||||
|
|
||||||
|
errors: ['NO_SUCH_USER', 'IS_ADMIN'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -22,11 +27,11 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
const user = await Users.findOneBy({ id: ps.userId });
|
const user = await Users.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new Error('user not found');
|
throw new ApiError('NO_SUCH_USER');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
throw new Error('cannot mark as moderator if admin user');
|
throw new ApiError('IS_ADMIN');
|
||||||
}
|
}
|
||||||
|
|
||||||
await Users.update(user.id, {
|
await Users.update(user.id, {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Users } from '@/models/index.js';
|
import { Users } from '@/models/index.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { publishInternalEvent } from '@/services/stream.js';
|
import { publishInternalEvent } from '@/services/stream.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
|
|
||||||
|
@ -7,6 +8,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireAdmin: true,
|
requireAdmin: true,
|
||||||
|
|
||||||
|
errors: ['NO_SUCH_USER'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -22,7 +25,7 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
const user = await Users.findOneBy({ id: ps.userId });
|
const user = await Users.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new Error('user not found');
|
throw new ApiError('NO_SUCH_USER');
|
||||||
}
|
}
|
||||||
|
|
||||||
await Users.update(user.id, {
|
await Users.update(user.id, {
|
||||||
|
|
|
@ -51,7 +51,7 @@ export const paramDef = {
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
try {
|
try {
|
||||||
if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only');
|
if (new URL(ps.inbox).protocol !== 'https:') throw new ApiError('INVALID_URL', 'https only');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new ApiError('INVALID_URL', e);
|
throw new ApiError('INVALID_URL', e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import bcrypt from 'bcryptjs';
|
import { hashPassword } from '@/misc/password.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import { Users, UserProfiles } from '@/models/index.js';
|
import { Users, UserProfiles } from '@/models/index.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -16,11 +17,11 @@ export const meta = {
|
||||||
password: {
|
password: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
minLength: 8,
|
|
||||||
maxLength: 8,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
errors: ['NO_SUCH_USER', 'IS_ADMIN'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -36,25 +37,22 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
const user = await Users.findOneBy({ id: ps.userId });
|
const user = await Users.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new Error('user not found');
|
throw new ApiError('NO_SUCH_USER');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
throw new Error('cannot reset password of admin');
|
throw new ApiError('IS_ADMIN');
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwd = secureRndstr(8, true);
|
const password = secureRndstr(8, true);
|
||||||
|
|
||||||
// Generate hash of password
|
|
||||||
const hash = bcrypt.hashSync(passwd);
|
|
||||||
|
|
||||||
await UserProfiles.update({
|
await UserProfiles.update({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
}, {
|
}, {
|
||||||
password: hash,
|
password: await hashPassword(password),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
password: passwd,
|
password,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Signins, UserProfiles, Users } from '@/models/index.js';
|
import { Signins, UserProfiles, Users } from '@/models/index.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -11,6 +12,8 @@ export const meta = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
errors: ['NO_SUCH_USER', 'IS_ADMIN'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -29,12 +32,12 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (user == null || profile == null) {
|
if (user == null || profile == null) {
|
||||||
throw new Error('user not found');
|
throw new ApiError('NO_SUCH_USER');
|
||||||
}
|
}
|
||||||
|
|
||||||
const _me = await Users.findOneByOrFail({ id: me.id });
|
const _me = await Users.findOneByOrFail({ id: me.id });
|
||||||
if ((_me.isModerator && !_me.isAdmin) && user.isAdmin) {
|
if ((_me.isModerator && !_me.isAdmin) && user.isAdmin) {
|
||||||
throw new Error('cannot show info of admin');
|
throw new ApiError('IS_ADMIN');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_me.isAdmin) {
|
if (!_me.isAdmin) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Users } from '@/models/index.js';
|
import { Users } from '@/models/index.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { insertModerationLog } from '@/services/insert-moderation-log.js';
|
import { insertModerationLog } from '@/services/insert-moderation-log.js';
|
||||||
import { publishInternalEvent } from '@/services/stream.js';
|
import { publishInternalEvent } from '@/services/stream.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
|
@ -8,6 +9,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
|
|
||||||
|
errors: ['NO_SUCH_USER', 'IS_ADMIN'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -23,11 +26,11 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
const user = await Users.findOneBy({ id: ps.userId });
|
const user = await Users.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new Error('user not found');
|
throw new ApiError('NO_SUCH_USER');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
throw new Error('cannot silence admin');
|
throw new ApiError('IS_ADMIN');
|
||||||
}
|
}
|
||||||
|
|
||||||
await Users.update(user.id, {
|
await Users.update(user.id, {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import deleteFollowing from '@/services/following/delete.js';
|
import deleteFollowing from '@/services/following/delete.js';
|
||||||
import { Users, Followings, Notifications } from '@/models/index.js';
|
import { Users, Followings, Notifications } from '@/models/index.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { insertModerationLog } from '@/services/insert-moderation-log.js';
|
import { insertModerationLog } from '@/services/insert-moderation-log.js';
|
||||||
import { doPostSuspend } from '@/services/suspend-user.js';
|
import { doPostSuspend } from '@/services/suspend-user.js';
|
||||||
import { publishUserEvent } from '@/services/stream.js';
|
import { publishUserEvent } from '@/services/stream.js';
|
||||||
|
@ -11,6 +12,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
|
|
||||||
|
errors: ['NO_SUCH_USER', 'IS_ADMIN', 'IS_MODERATOR'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -26,15 +29,11 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
const user = await Users.findOneBy({ id: ps.userId });
|
const user = await Users.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new Error('user not found');
|
throw new ApiError('NO_SUCH_USER');
|
||||||
}
|
} else if (user.isAdmin) {
|
||||||
|
throw new ApiError('IS_ADMIN');
|
||||||
if (user.isAdmin) {
|
} else if (user.isModerator) {
|
||||||
throw new Error('cannot suspend admin');
|
throw new ApiError('IS_MODERATOR');
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isModerator) {
|
|
||||||
throw new Error('cannot suspend moderator');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Users.update(user.id, {
|
await Users.update(user.id, {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Users } from '@/models/index.js';
|
import { Users } from '@/models/index.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { insertModerationLog } from '@/services/insert-moderation-log.js';
|
import { insertModerationLog } from '@/services/insert-moderation-log.js';
|
||||||
import { publishInternalEvent } from '@/services/stream.js';
|
import { publishInternalEvent } from '@/services/stream.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
|
@ -8,6 +9,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
|
|
||||||
|
errors: ['NO_SUCH_USER'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -23,7 +26,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
const user = await Users.findOneBy({ id: ps.userId });
|
const user = await Users.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new Error('user not found');
|
throw new ApiError('NO_SUCH_USER');
|
||||||
}
|
}
|
||||||
|
|
||||||
await Users.update(user.id, {
|
await Users.update(user.id, {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Users } from '@/models/index.js';
|
import { Users } from '@/models/index.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { insertModerationLog } from '@/services/insert-moderation-log.js';
|
import { insertModerationLog } from '@/services/insert-moderation-log.js';
|
||||||
import { doPostUnsuspend } from '@/services/unsuspend-user.js';
|
import { doPostUnsuspend } from '@/services/unsuspend-user.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
|
@ -8,6 +9,8 @@ export const meta = {
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireModerator: true,
|
requireModerator: true,
|
||||||
|
|
||||||
|
errors: ['NO_SUCH_USER'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -23,7 +26,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
const user = await Users.findOneBy({ id: ps.userId });
|
const user = await Users.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new Error('user not found');
|
throw new ApiError('NO_SUCH_USER');
|
||||||
}
|
}
|
||||||
|
|
||||||
await Users.update(user.id, {
|
await Users.update(user.id, {
|
||||||
|
|
|
@ -29,6 +29,6 @@ export const paramDef = {
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps) => {
|
export default define(meta, paramDef, async (ps) => {
|
||||||
const resolver = new Resolver();
|
const resolver = new Resolver();
|
||||||
const object = await resolver.resolve(ps.uri);
|
const object = await resolver.resolve(ps.uri, true);
|
||||||
return object;
|
return object;
|
||||||
});
|
});
|
||||||
|
|
|
@ -98,9 +98,10 @@ async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined):
|
||||||
]));
|
]));
|
||||||
if (local != null) return local;
|
if (local != null) return local;
|
||||||
|
|
||||||
// リモートから一旦オブジェクトフェッチ
|
// fetch object from remote
|
||||||
const resolver = new Resolver();
|
const resolver = new Resolver();
|
||||||
const object = await resolver.resolve(uri) as any;
|
// allow redirect
|
||||||
|
const object = await resolver.resolve(uri, true) as any;
|
||||||
|
|
||||||
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
|
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
|
||||||
// これはDBに存在する可能性があるため再度DB検索
|
// これはDBに存在する可能性があるため再度DB検索
|
||||||
|
|
|
@ -35,7 +35,6 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const instance = await fetchMeta(true);
|
const instance = await fetchMeta(true);
|
||||||
|
|
||||||
// Calculate drive usage
|
|
||||||
const usage = await DriveFiles.calcDriveUsageOf(user.id);
|
const usage = await DriveFiles.calcDriveUsageOf(user.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
|
||||||
if (ps.type) {
|
if (ps.type) {
|
||||||
if (ps.type.endsWith('/*')) {
|
if (ps.type.endsWith('/*')) {
|
||||||
query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' });
|
query.andWhere('file.type like :type', { type: ps.type.slice(0, -1) + '%' });
|
||||||
} else {
|
} else {
|
||||||
query.andWhere('file.type = :type', { type: ps.type });
|
query.andWhere('file.type = :type', { type: ps.type });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import acceptFollowRequest from '@/services/following/requests/accept.js';
|
import { acceptFollowRequest } from '@/services/following/requests/accept.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
import { ApiError } from '../../../error.js';
|
import { ApiError } from '../../../error.js';
|
||||||
import { getUser } from '../../../common/getters.js';
|
import { getUser } from '../../../common/getters.js';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import cancelFollowRequest from '@/services/following/requests/cancel.js';
|
import { cancelFollowRequest } from '@/services/following/requests/cancel.js';
|
||||||
import { Users } from '@/models/index.js';
|
import { Users } from '@/models/index.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { genId } from '@/misc/gen-id.js';
|
||||||
import { GalleryPost } from '@/models/entities/gallery-post.js';
|
import { GalleryPost } from '@/models/entities/gallery-post.js';
|
||||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||||
import { HOUR } from '@/const.js';
|
import { HOUR } from '@/const.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -46,8 +47,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
}),
|
}),
|
||||||
))).filter((file): file is DriveFile => file != null);
|
))).filter((file): file is DriveFile => file != null);
|
||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length !== ps.fileIds.length) {
|
||||||
throw new Error();
|
throw new ApiError(
|
||||||
|
'INVALID_PARAM',
|
||||||
|
{
|
||||||
|
param: '#/properties/fileIds/items',
|
||||||
|
reason: 'contains invalid file IDs',
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = await GalleryPosts.insert(new GalleryPost({
|
const post = await GalleryPosts.insert(new GalleryPost({
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { DriveFiles, GalleryPosts } from '@/models/index.js';
|
import { DriveFiles, GalleryPosts } from '@/models/index.js';
|
||||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||||
import { HOUR } from '@/const.js';
|
import { HOUR } from '@/const.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -20,6 +21,8 @@ export const meta = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'GalleryPost',
|
ref: 'GalleryPost',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
errors: ['INVALID_PARAM'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -45,8 +48,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
}),
|
}),
|
||||||
))).filter((file): file is DriveFile => file != null);
|
))).filter((file): file is DriveFile => file != null);
|
||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length !== ps.fileIds.length) {
|
||||||
throw new Error();
|
throw new ApiError(
|
||||||
|
'INVALID_PARAM',
|
||||||
|
{
|
||||||
|
param: '#/properties/fileIds/items',
|
||||||
|
reason: 'contains invalid file IDs',
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await GalleryPosts.update({
|
await GalleryPosts.update({
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import * as speakeasy from 'speakeasy';
|
import * as speakeasy from 'speakeasy';
|
||||||
import { UserProfiles } from '@/models/index.js';
|
import { UserProfiles } from '@/models/index.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
secure: true,
|
secure: true,
|
||||||
|
|
||||||
|
errors: ['INTERNAL_ERROR', 'ACCESS_DENIED'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -23,7 +26,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
|
|
||||||
if (profile.twoFactorTempSecret == null) {
|
if (profile.twoFactorTempSecret == null) {
|
||||||
throw new Error('二段階認証の設定が開始されていません');
|
throw new ApiError('INTERNAL_ERROR', 'Two-step verification has not been initiated.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const verified = (speakeasy as any).totp.verify({
|
const verified = (speakeasy as any).totp.verify({
|
||||||
|
@ -33,7 +36,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
throw new Error('not verified');
|
throw new ApiError('ACCESS_DENIED', 'TOTP missmatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
await UserProfiles.update(user.id, {
|
await UserProfiles.update(user.id, {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import bcrypt from 'bcryptjs';
|
|
||||||
import * as cbor from 'cbor';
|
import * as cbor from 'cbor';
|
||||||
import { MINUTE } from '@/const.js';
|
import { MINUTE } from '@/const.js';
|
||||||
|
import { comparePassword } from '@/misc/password.js';
|
||||||
import {
|
import {
|
||||||
UserProfiles,
|
UserProfiles,
|
||||||
UserSecurityKeys,
|
UserSecurityKeys,
|
||||||
|
@ -9,6 +9,7 @@ import {
|
||||||
Users,
|
Users,
|
||||||
} from '@/models/index.js';
|
} from '@/models/index.js';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { publishMainStream } from '@/services/stream.js';
|
import { publishMainStream } from '@/services/stream.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
import { procedures, hash } from '../../../2fa.js';
|
import { procedures, hash } from '../../../2fa.js';
|
||||||
|
@ -20,6 +21,8 @@ export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
secure: true,
|
secure: true,
|
||||||
|
|
||||||
|
errors: ['ACCESS_DENIED', 'INTERNAL_ERROR', 'NO_SUCH_OBJECT'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -38,24 +41,21 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
|
|
||||||
// Compare password
|
if (!(await comparePassword(ps.password, profile.password!))) {
|
||||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
throw new ApiError('ACCESS_DENIED');
|
||||||
|
|
||||||
if (!same) {
|
|
||||||
throw new Error('incorrect password');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!profile.twoFactorEnabled) {
|
if (!profile.twoFactorEnabled) {
|
||||||
throw new Error('2fa not enabled');
|
throw new ApiError('INTERNAL_ERROR', '2fa not enabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientData = JSON.parse(ps.clientDataJSON);
|
const clientData = JSON.parse(ps.clientDataJSON);
|
||||||
|
|
||||||
if (clientData.type !== 'webauthn.create') {
|
if (clientData.type !== 'webauthn.create') {
|
||||||
throw new Error('not a creation attestation');
|
throw new ApiError('INTERNAL_ERROR', 'not a creation attestation');
|
||||||
}
|
}
|
||||||
if (clientData.origin !== config.scheme + '://' + config.host) {
|
if (clientData.origin !== config.scheme + '://' + config.host) {
|
||||||
throw new Error('origin mismatch');
|
throw new ApiError('INTERNAL_ERROR', 'origin mismatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientDataJSONHash = hash(Buffer.from(ps.clientDataJSON, 'utf-8'));
|
const clientDataJSONHash = hash(Buffer.from(ps.clientDataJSON, 'utf-8'));
|
||||||
|
@ -64,14 +64,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
|
||||||
const rpIdHash = attestation.authData.slice(0, 32);
|
const rpIdHash = attestation.authData.slice(0, 32);
|
||||||
if (!rpIdHashReal.equals(rpIdHash)) {
|
if (!rpIdHashReal.equals(rpIdHash)) {
|
||||||
throw new Error('rpIdHash mismatch');
|
throw new ApiError('INTERNAL_ERROR', 'rpIdHash mismatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
const flags = attestation.authData[32];
|
const flags = attestation.authData[32];
|
||||||
|
|
||||||
// eslint:disable-next-line:no-bitwise
|
// eslint:disable-next-line:no-bitwise
|
||||||
if (!(flags & 1)) {
|
if (!(flags & 1)) {
|
||||||
throw new Error('user not present');
|
throw new ApiError('INTERNAL_ERROR', 'user not present');
|
||||||
}
|
}
|
||||||
|
|
||||||
const authData = Buffer.from(attestation.authData);
|
const authData = Buffer.from(attestation.authData);
|
||||||
|
@ -80,11 +80,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
const publicKeyData = authData.slice(55 + credentialIdLength);
|
const publicKeyData = authData.slice(55 + credentialIdLength);
|
||||||
const publicKey: Map<number, any> = await cborDecodeFirst(publicKeyData);
|
const publicKey: Map<number, any> = await cborDecodeFirst(publicKeyData);
|
||||||
if (publicKey.get(3) !== -7) {
|
if (publicKey.get(3) !== -7) {
|
||||||
throw new Error('alg mismatch');
|
throw new ApiError('INTERNAL_ERROR', 'algorithm mismatch');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(procedures as any)[attestation.fmt]) {
|
if (!(procedures as any)[attestation.fmt]) {
|
||||||
throw new Error('unsupported fmt');
|
throw new ApiError('INTERNAL_ERROR', 'unsupported fmt');
|
||||||
}
|
}
|
||||||
|
|
||||||
const verificationData = (procedures as any)[attestation.fmt].verify({
|
const verificationData = (procedures as any)[attestation.fmt].verify({
|
||||||
|
@ -95,7 +95,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
publicKey,
|
publicKey,
|
||||||
rpIdHash,
|
rpIdHash,
|
||||||
});
|
});
|
||||||
if (!verificationData.valid) throw new Error('signature invalid');
|
if (!verificationData.valid) throw new ApiError('INTERNAL_ERROR', 'signature invalid');
|
||||||
|
|
||||||
const attestationChallenge = await AttestationChallenges.findOneBy({
|
const attestationChallenge = await AttestationChallenges.findOneBy({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
@ -105,7 +105,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!attestationChallenge) {
|
if (!attestationChallenge) {
|
||||||
throw new Error('non-existent challenge');
|
throw new ApiError('NO_SUCH_OBJECT', 'Attestation challenge not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await AttestationChallenges.delete({
|
await AttestationChallenges.delete({
|
||||||
|
@ -118,7 +118,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
new Date().getTime() - attestationChallenge.createdAt.getTime() >=
|
new Date().getTime() - attestationChallenge.createdAt.getTime() >=
|
||||||
5 * MINUTE
|
5 * MINUTE
|
||||||
) {
|
) {
|
||||||
throw new Error('expired challenge');
|
throw new ApiError('NO_SUCH_OBJECT', 'Attestation challenge expired.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentialIdString = credentialId.toString('hex');
|
const credentialIdString = credentialId.toString('hex');
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import * as crypto from 'node:crypto';
|
import * as crypto from 'node:crypto';
|
||||||
import bcrypt from 'bcryptjs';
|
|
||||||
import { UserProfiles, AttestationChallenges } from '@/models/index.js';
|
import { UserProfiles, AttestationChallenges } from '@/models/index.js';
|
||||||
import { genId } from '@/misc/gen-id.js';
|
import { genId } from '@/misc/gen-id.js';
|
||||||
|
import { comparePassword } from '@/misc/password.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
import { hash } from '../../../2fa.js';
|
import { hash } from '../../../2fa.js';
|
||||||
|
|
||||||
|
@ -12,6 +13,8 @@ export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
secure: true,
|
secure: true,
|
||||||
|
|
||||||
|
errors: ['ACCESS_DENIED', 'INTERNAL_ERROR'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -26,15 +29,12 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
|
|
||||||
// Compare password
|
if (!(await comparePassword(ps.password, profile.password!))) {
|
||||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
throw new ApiError('ACCESS_DENIED');
|
||||||
|
|
||||||
if (!same) {
|
|
||||||
throw new Error('incorrect password');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!profile.twoFactorEnabled) {
|
if (!profile.twoFactorEnabled) {
|
||||||
throw new Error('2fa not enabled');
|
throw new ApiError('INTERNAL_ERROR', '2fa not enabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 32 byte challenge
|
// 32 byte challenge
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import bcrypt from 'bcryptjs';
|
|
||||||
import * as speakeasy from 'speakeasy';
|
import * as speakeasy from 'speakeasy';
|
||||||
import * as QRCode from 'qrcode';
|
import * as QRCode from 'qrcode';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
|
import { comparePassword } from '@/misc/password.js';
|
||||||
import { UserProfiles } from '@/models/index.js';
|
import { UserProfiles } from '@/models/index.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
secure: true,
|
secure: true,
|
||||||
|
|
||||||
|
errors: ['ACCESS_DENIED'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -23,11 +26,8 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
|
|
||||||
// Compare password
|
if (!(await comparePassword(ps.password, profile.password!))) {
|
||||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
throw new ApiError('ACCESS_DENIED');
|
||||||
|
|
||||||
if (!same) {
|
|
||||||
throw new Error('incorrect password');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate user's secret key
|
// Generate user's secret key
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import bcrypt from 'bcryptjs';
|
import { comparePassword } from '@/misc/password.js';
|
||||||
import { UserProfiles, UserSecurityKeys, Users } from '@/models/index.js';
|
import { UserProfiles, UserSecurityKeys, Users } from '@/models/index.js';
|
||||||
import { publishMainStream } from '@/services/stream.js';
|
import { publishMainStream } from '@/services/stream.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
secure: true,
|
secure: true,
|
||||||
|
|
||||||
|
errors: ['ACCESS_DENIED'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -22,11 +25,8 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
|
|
||||||
// Compare password
|
if (!(await comparePassword(ps.password, profile.password!))) {
|
||||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
throw new ApiError('ACCESS_DENIED');
|
||||||
|
|
||||||
if (!same) {
|
|
||||||
throw new Error('incorrect password');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we only delete the user's own creds
|
// Make sure we only delete the user's own creds
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import bcrypt from 'bcryptjs';
|
import { comparePassword } from '@/misc/password.js';
|
||||||
import { UserProfiles } from '@/models/index.js';
|
import { UserProfiles } from '@/models/index.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import define from '../../../define.js';
|
import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
secure: true,
|
secure: true,
|
||||||
|
|
||||||
|
errors: ['ACCESS_DENIED'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -20,11 +23,8 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
|
|
||||||
// Compare password
|
if (!(await comparePassword(ps.password, profile.password!))) {
|
||||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
throw new ApiError('ACCESS_DENIED');
|
||||||
|
|
||||||
if (!same) {
|
|
||||||
throw new Error('incorrect password');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await UserProfiles.update(user.id, {
|
await UserProfiles.update(user.id, {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import bcrypt from 'bcryptjs';
|
import { comparePassword, hashPassword } from '@/misc/password.js';
|
||||||
import { UserProfiles } from '@/models/index.js';
|
import { UserProfiles } from '@/models/index.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
secure: true,
|
secure: true,
|
||||||
|
|
||||||
|
errors: ['ACCESS_DENIED'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -21,18 +24,11 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
|
|
||||||
// Compare password
|
if (!(await comparePassword(ps.currentPassword, profile.password!))) {
|
||||||
const same = await bcrypt.compare(ps.currentPassword, profile.password!);
|
throw new ApiError('ACCESS_DENIED');
|
||||||
|
|
||||||
if (!same) {
|
|
||||||
throw new Error('incorrect password');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate hash of password
|
|
||||||
const salt = await bcrypt.genSalt(8);
|
|
||||||
const hash = await bcrypt.hash(ps.newPassword, salt);
|
|
||||||
|
|
||||||
await UserProfiles.update(user.id, {
|
await UserProfiles.update(user.id, {
|
||||||
password: hash,
|
password: await hashPassword(ps.newPassword),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import bcrypt from 'bcryptjs';
|
import { comparePassword } from '@/misc/password.js';
|
||||||
import { UserProfiles, Users } from '@/models/index.js';
|
import { UserProfiles, Users } from '@/models/index.js';
|
||||||
import { deleteAccount } from '@/services/delete-account.js';
|
import { deleteAccount } from '@/services/delete-account.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
secure: true,
|
secure: true,
|
||||||
|
|
||||||
|
errors: ['ACCESS_DENIED'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -19,17 +22,17 @@ export const paramDef = {
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
const [profile, userDetailed] = await Promise.all([
|
||||||
const userDetailed = await Users.findOneByOrFail({ id: user.id });
|
UserProfiles.findOneByOrFail({ userId: user.id }),
|
||||||
|
Users.findOneByOrFail({ id: user.id }),
|
||||||
|
]);
|
||||||
|
|
||||||
if (userDetailed.isDeleted) {
|
if (userDetailed.isDeleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare password
|
if (!(await comparePassword(ps.password, profile.password!))) {
|
||||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
throw new ApiError('ACCESS_DENIED');
|
||||||
|
|
||||||
if (!same) {
|
|
||||||
throw new Error('incorrect password');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteAccount(user);
|
await deleteAccount(user);
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import bcrypt from 'bcryptjs';
|
import { comparePassword } from '@/misc/password.js';
|
||||||
import { publishInternalEvent, publishMainStream, publishUserEvent } from '@/services/stream.js';
|
import { publishInternalEvent, publishMainStream, publishUserEvent } from '@/services/stream.js';
|
||||||
import { Users, UserProfiles } from '@/models/index.js';
|
import { Users, UserProfiles } from '@/models/index.js';
|
||||||
import generateUserToken from '../../common/generate-native-user-token.js';
|
import generateUserToken from '../../common/generate-native-user-token.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
|
||||||
secure: true,
|
secure: true,
|
||||||
|
|
||||||
|
errors: ['ACCESS_DENIED'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -25,11 +28,8 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
|
|
||||||
// Compare password
|
if (!(await comparePassword(ps.password, profile.password!))) {
|
||||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
throw new ApiError('ACCESS_DENIED');
|
||||||
|
|
||||||
if (!same) {
|
|
||||||
throw new Error('incorrect password');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newToken = generateUserToken();
|
const newToken = generateUserToken();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import bcrypt from 'bcryptjs';
|
|
||||||
import { publishMainStream } from '@/services/stream.js';
|
import { publishMainStream } from '@/services/stream.js';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
|
import { comparePassword } from '@/misc/password.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import { Users, UserProfiles } from '@/models/index.js';
|
import { Users, UserProfiles } from '@/models/index.js';
|
||||||
import { sendEmail } from '@/services/send-email.js';
|
import { sendEmail } from '@/services/send-email.js';
|
||||||
|
@ -37,10 +37,9 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
|
|
||||||
// Compare password
|
if (!(await comparePassword(ps.password, profile.password!))) {
|
||||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
throw new ApiError('ACCESS_DENIED');
|
||||||
|
}
|
||||||
if (!same) throw new ApiError('ACCESS_DENIED');
|
|
||||||
|
|
||||||
if (ps.email != null) {
|
if (ps.email != null) {
|
||||||
const available = await validateEmailForAccount(ps.email);
|
const available = await validateEmailForAccount(ps.email);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import RE2 from 're2';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import { notificationTypes } from 'foundkey-js';
|
import { notificationTypes } from 'foundkey-js';
|
||||||
import { publishMainStream, publishUserEvent } from '@/services/stream.js';
|
import { publishMainStream, publishUserEvent } from '@/services/stream.js';
|
||||||
import acceptAllFollowRequests from '@/services/following/requests/accept-all.js';
|
import { acceptAllFollowRequests } from '@/services/following/requests/accept-all.js';
|
||||||
import { publishToFollowers } from '@/services/i/update.js';
|
import { publishToFollowers } from '@/services/i/update.js';
|
||||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||||
|
|
|
@ -233,6 +233,10 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
v2: {
|
||||||
|
method: 'get'
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -21,6 +21,11 @@ export const meta = {
|
||||||
ref: 'Note',
|
ref: 'Note',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
v2: {
|
||||||
|
method: 'get',
|
||||||
|
alias: 'notes/:noteId/children',
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
@ -51,9 +56,7 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||||
.andWhere('note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))', { noteId: ps.noteId, depth: ps.depth, limit: ps.limit })
|
.andWhere('note.id IN (SELECT id FROM note_replies(:noteId, :depth, :limit))', { noteId: ps.noteId, depth: ps.depth, limit: ps.limit })
|
||||||
.innerJoinAndSelect('note.user', 'user')
|
.innerJoinAndSelect('note.user', 'user');
|
||||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
|
||||||
.leftJoinAndSelect('user.banner', 'banner');
|
|
||||||
|
|
||||||
generateVisibilityQuery(query, user);
|
generateVisibilityQuery(query, user);
|
||||||
if (user) {
|
if (user) {
|
||||||
|
|
|
@ -19,6 +19,11 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
v2: {
|
||||||
|
method: 'get',
|
||||||
|
alias: 'notes/:noteId/clips',
|
||||||
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_NOTE'],
|
errors: ['NO_SUCH_NOTE'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,11 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
v2: {
|
||||||
|
method: 'get',
|
||||||
|
alias: 'notes/:noteId/conversation',
|
||||||
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_NOTE'],
|
errors: ['NO_SUCH_NOTE'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,11 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
v2: {
|
||||||
|
method: 'post',
|
||||||
|
alias: 'notes',
|
||||||
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_NOTE', 'PURE_RENOTE', 'EXPIRED_POLL', 'NO_SUCH_CHANNEL', 'BLOCKED', 'LESS_RESTRICTIVE_VISIBILITY'],
|
errors: ['NO_SUCH_NOTE', 'PURE_RENOTE', 'EXPIRED_POLL', 'NO_SUCH_CHANNEL', 'BLOCKED', 'LESS_RESTRICTIVE_VISIBILITY'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,11 @@ export const meta = {
|
||||||
minInterval: SECOND,
|
minInterval: SECOND,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
v2: {
|
||||||
|
method: 'delete',
|
||||||
|
alias: 'notes/:noteId',
|
||||||
|
},
|
||||||
|
|
||||||
errors: ['ACCESS_DENIED', 'NO_SUCH_NOTE'],
|
errors: ['ACCESS_DENIED', 'NO_SUCH_NOTE'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,10 @@ export const meta = {
|
||||||
ref: 'Note',
|
ref: 'Note',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
v2: {
|
||||||
|
method: 'get',
|
||||||
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -23,6 +23,11 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
v2: {
|
||||||
|
method: 'get',
|
||||||
|
alias: 'notes/:noteId/reactions/:type?',
|
||||||
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_NOTE'],
|
errors: ['NO_SUCH_NOTE'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,11 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
v2: {
|
||||||
|
method: 'get',
|
||||||
|
alias: 'notes/:noteId/renotes',
|
||||||
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_NOTE'],
|
errors: ['NO_SUCH_NOTE'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,11 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
v2: {
|
||||||
|
method: 'get',
|
||||||
|
alias: 'notes/:noteId/replies',
|
||||||
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_NOTE'],
|
errors: ['NO_SUCH_NOTE'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,11 @@ export const meta = {
|
||||||
ref: 'Note',
|
ref: 'Note',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
v2: {
|
||||||
|
method: 'get',
|
||||||
|
alias: 'notes/:noteId',
|
||||||
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_NOTE'],
|
errors: ['NO_SUCH_NOTE'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,11 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
v2: {
|
||||||
|
method: 'get',
|
||||||
|
alias: 'notes/:noteId/status',
|
||||||
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_NOTE'],
|
errors: ['NO_SUCH_NOTE'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,11 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
v2: {
|
||||||
|
method: 'get',
|
||||||
|
alias: 'notes/:noteId/translate/:targetLang/:sourceLang?',
|
||||||
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_NOTE'],
|
errors: ['NO_SUCH_NOTE'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,11 @@ export const meta = {
|
||||||
minInterval: SECOND,
|
minInterval: SECOND,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
v2: {
|
||||||
|
method: 'delete',
|
||||||
|
alias: 'notes/:noteId/renotes',
|
||||||
|
},
|
||||||
|
|
||||||
errors: ['NO_SUCH_NOTE'],
|
errors: ['NO_SUCH_NOTE'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { resetDb } from '@/db/postgre.js';
|
import { resetDb } from '@/db/postgre.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import define from '../define.js';
|
import define from '../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -17,7 +18,7 @@ export const paramDef = {
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps, user) => {
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
if (process.env.NODE_ENV !== 'test') throw new Error('NODE_ENV is not a test');
|
if (process.env.NODE_ENV !== 'test') throw new ApiError('ACCESS_DENIED');
|
||||||
|
|
||||||
await resetDb();
|
await resetDb();
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import bcrypt from 'bcryptjs';
|
import { hashPassword } from '@/misc/password.js';
|
||||||
import { UserProfiles, PasswordResetRequests } from '@/models/index.js';
|
import { UserProfiles, PasswordResetRequests } from '@/models/index.js';
|
||||||
import { DAY, MINUTE } from '@/const.js';
|
import { DAY, MINUTE } from '@/const.js';
|
||||||
import define from '../define.js';
|
import define from '../define.js';
|
||||||
|
@ -43,12 +43,8 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
throw new ApiError('NO_SUCH_RESET_REQUEST');
|
throw new ApiError('NO_SUCH_RESET_REQUEST');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate hash of password
|
|
||||||
const salt = await bcrypt.genSalt(8);
|
|
||||||
const hash = await bcrypt.hash(ps.password, salt);
|
|
||||||
|
|
||||||
await UserProfiles.update(req.userId, {
|
await UserProfiles.update(req.userId, {
|
||||||
password: hash,
|
password: await hashPassword(ps.password),
|
||||||
});
|
});
|
||||||
|
|
||||||
await PasswordResetRequests.delete(req.id);
|
await PasswordResetRequests.delete(req.id);
|
||||||
|
|
|
@ -177,7 +177,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
driveFilesCount: DriveFiles.createQueryBuilder('file')
|
driveFilesCount: DriveFiles.createQueryBuilder('file')
|
||||||
.where('file.userId = :userId', { userId: user.id })
|
.where('file.userId = :userId', { userId: user.id })
|
||||||
.getCount(),
|
.getCount(),
|
||||||
driveUsage: DriveFiles.calcDriveUsageOf(user),
|
driveUsage: DriveFiles.calcDriveUsageOf(user.id),
|
||||||
});
|
});
|
||||||
|
|
||||||
result.followingCount = result.localFollowingCount + result.remoteFollowingCount;
|
result.followingCount = result.localFollowingCount + result.remoteFollowingCount;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import Koa from 'koa';
|
||||||
|
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
public message: string;
|
public message: string;
|
||||||
public code: string;
|
public code: string;
|
||||||
|
@ -20,6 +22,24 @@ export class ApiError extends Error {
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.httpStatusCode = httpStatusCode;
|
this.httpStatusCode = httpStatusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the response of ctx the current error, given the respective endpoint name.
|
||||||
|
*/
|
||||||
|
public apply(ctx: Koa.Context, endpoint: string): void {
|
||||||
|
ctx.status = this.httpStatusCode;
|
||||||
|
if (ctx.status === 401) {
|
||||||
|
ctx.response.set('WWW-Authenticate', 'Bearer');
|
||||||
|
}
|
||||||
|
ctx.body = {
|
||||||
|
error: {
|
||||||
|
message: this.message,
|
||||||
|
code: this.code,
|
||||||
|
info: this.info ?? undefined,
|
||||||
|
endpoint,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const errors: Record<string, { message: string, httpStatusCode: number }> = {
|
export const errors: Record<string, { message: string, httpStatusCode: number }> = {
|
||||||
|
@ -167,6 +187,14 @@ export const errors: Record<string, { message: string, httpStatusCode: number }>
|
||||||
message: 'Invalid username.',
|
message: 'Invalid username.',
|
||||||
httpStatusCode: 400,
|
httpStatusCode: 400,
|
||||||
},
|
},
|
||||||
|
IS_ADMIN: {
|
||||||
|
message: 'This action cannot be done to an administrator account.',
|
||||||
|
httpStatusCode: 400,
|
||||||
|
},
|
||||||
|
IS_MODERATOR: {
|
||||||
|
message: 'This action cannot be done to a moderator account.',
|
||||||
|
httpStatusCode: 400,
|
||||||
|
},
|
||||||
LESS_RESTRICTIVE_VISIBILITY: {
|
LESS_RESTRICTIVE_VISIBILITY: {
|
||||||
message: 'The visibility cannot be less restrictive than the parent note.',
|
message: 'The visibility cannot be less restrictive than the parent note.',
|
||||||
httpStatusCode: 400,
|
httpStatusCode: 400,
|
||||||
|
|
|
@ -16,6 +16,7 @@ import signup from './private/signup.js';
|
||||||
import signin from './private/signin.js';
|
import signin from './private/signin.js';
|
||||||
import signupPending from './private/signup-pending.js';
|
import signupPending from './private/signup-pending.js';
|
||||||
import { oauth } from './common/oauth.js';
|
import { oauth } from './common/oauth.js';
|
||||||
|
import { ApiError } from './error.js';
|
||||||
|
|
||||||
// Init app
|
// Init app
|
||||||
const app = new Koa();
|
const app = new Koa();
|
||||||
|
@ -40,6 +41,27 @@ const upload = multer({
|
||||||
files: 1,
|
files: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
/**
|
||||||
|
* Wrap multer to return an appropriate API error when something goes wrong, e.g. the file is too big.
|
||||||
|
*/
|
||||||
|
type KoaMiddleware = (ctx: Koa.Context, next: () => Promise<void>) => Promise<void>;
|
||||||
|
const wrapped = upload.single('file');
|
||||||
|
function uploadWrapper(endpoint: string): KoaMiddleware {
|
||||||
|
return (ctx: Koa.Context, next: () => Promise<void>): Promise<void> => {
|
||||||
|
// pass a fake "next" so we can separate multer errors from other API errors
|
||||||
|
return wrapped(ctx, () => {})
|
||||||
|
.then(
|
||||||
|
() => next(),
|
||||||
|
(err) => {
|
||||||
|
let apiErr = new ApiError('INTERNAL_ERROR', err);
|
||||||
|
if (err?.code === 'LIMIT_FILE_SIZE') {
|
||||||
|
apiErr = new ApiError('FILE_TOO_BIG', { maxFileSize: config.maxFileSize || 262144000 });
|
||||||
|
}
|
||||||
|
apiErr.apply(ctx, endpoint);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Init router
|
// Init router
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
@ -49,7 +71,7 @@ const router = new Router();
|
||||||
*/
|
*/
|
||||||
for (const endpoint of endpoints) {
|
for (const endpoint of endpoints) {
|
||||||
if (endpoint.meta.requireFile) {
|
if (endpoint.meta.requireFile) {
|
||||||
router.post(`/${endpoint.name}`, upload.single('file'), handler.bind(null, endpoint));
|
router.post(`/${endpoint.name}`, uploadWrapper(endpoint.name), handler.bind(null, endpoint));
|
||||||
} else {
|
} else {
|
||||||
// 後方互換性のため
|
// 後方互換性のため
|
||||||
if (endpoint.name.includes('-')) {
|
if (endpoint.name.includes('-')) {
|
||||||
|
@ -69,6 +91,11 @@ for (const endpoint of endpoints) {
|
||||||
} else {
|
} else {
|
||||||
router.get(`/${endpoint.name}`, async ctx => { ctx.status = 405; });
|
router.get(`/${endpoint.name}`, async ctx => { ctx.status = 405; });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (endpoint.meta.v2) {
|
||||||
|
const path = endpoint.meta.v2.alias ?? endpoint.name.replace(/-/g, '_');
|
||||||
|
router[endpoint.meta.v2.method](`/v2/${path}`, handler.bind(null, endpoint));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -207,6 +207,19 @@ export function genOpenapiSpec() {
|
||||||
}
|
}
|
||||||
|
|
||||||
spec.paths['/' + endpoint.name] = path;
|
spec.paths['/' + endpoint.name] = path;
|
||||||
|
|
||||||
|
if (endpoint.meta.v2) {
|
||||||
|
// we need a clone of the API endpoint info because otherwise we change it by reference
|
||||||
|
const infoClone = structuredClone(info);
|
||||||
|
const route = `/v2/${endpoint.meta.v2.alias ?? endpoint.name.replace(/-/g, '_')}`;
|
||||||
|
|
||||||
|
infoClone['operationId'] = infoClone['summary'] = route;
|
||||||
|
|
||||||
|
spec.paths[route] = {
|
||||||
|
...spec.paths[route],
|
||||||
|
[endpoint.meta.v2.method]: infoClone,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return spec;
|
return spec;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import Koa from 'koa';
|
import Koa from 'koa';
|
||||||
import bcrypt from 'bcryptjs';
|
|
||||||
import * as speakeasy from 'speakeasy';
|
import * as speakeasy from 'speakeasy';
|
||||||
import { SECOND, MINUTE, HOUR } from '@/const.js';
|
import { SECOND, MINUTE, HOUR } from '@/const.js';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
|
@ -9,10 +8,11 @@ import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges }
|
||||||
import { ILocalUser } from '@/models/entities/user.js';
|
import { ILocalUser } from '@/models/entities/user.js';
|
||||||
import { genId } from '@/misc/gen-id.js';
|
import { genId } from '@/misc/gen-id.js';
|
||||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||||
|
import { comparePassword } from '@/misc/password.js';
|
||||||
import signin from '../common/signin.js';
|
import signin from '../common/signin.js';
|
||||||
import { verifyLogin, hash } from '../2fa.js';
|
import { verifyLogin, hash } from '../2fa.js';
|
||||||
import { limiter } from '../limiter.js';
|
import { limiter } from '../limiter.js';
|
||||||
import { ApiError } from '../error.js';
|
import { ApiError, errors } from '../error.js';
|
||||||
|
|
||||||
export default async (ctx: Koa.Context) => {
|
export default async (ctx: Koa.Context) => {
|
||||||
ctx.set('Access-Control-Allow-Origin', config.url);
|
ctx.set('Access-Control-Allow-Origin', config.url);
|
||||||
|
@ -21,42 +21,30 @@ export default async (ctx: Koa.Context) => {
|
||||||
const body = ctx.request.body as any;
|
const body = ctx.request.body as any;
|
||||||
const { username, password, token } = body;
|
const { username, password, token } = body;
|
||||||
|
|
||||||
// taken from @server/api/api-handler.ts
|
function error(e: keyof errors, info?: Record<string, any>): void {
|
||||||
function error (e: ApiError): void {
|
new ApiError(e, info).apply(ctx, 'signin');
|
||||||
ctx.status = e.httpStatusCode;
|
|
||||||
if (e.httpStatusCode === 401) {
|
|
||||||
ctx.response.set('WWW-Authenticate', 'Bearer');
|
|
||||||
}
|
|
||||||
ctx.body = {
|
|
||||||
error: {
|
|
||||||
message: e!.message,
|
|
||||||
code: e!.code,
|
|
||||||
...(e!.info ? { info: e!.info } : {}),
|
|
||||||
endpoint: 'signin',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// not more than 1 attempt per second and not more than 10 attempts per hour
|
// not more than 1 attempt per second and not more than 10 attempts per hour
|
||||||
await limiter({ key: 'signin', duration: HOUR, max: 10, minInterval: SECOND }, getIpHash(ctx.ip));
|
await limiter({ key: 'signin', duration: HOUR, max: 10, minInterval: SECOND }, getIpHash(ctx.ip));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error(new ApiError('RATE_LIMIT_EXCEEDED'));
|
error('RATE_LIMIT_EXCEEDED');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof username !== 'string') {
|
if (typeof username !== 'string') {
|
||||||
error(new ApiError('INVALID_PARAM', { param: 'username', reason: 'not a string' }));
|
error('INVALID_PARAM', { param: 'username', reason: 'not a string' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof password !== 'string') {
|
if (typeof password !== 'string') {
|
||||||
error(new ApiError('INVALID_PARAM', { param: 'password', reason: 'not a string' }));
|
error('INVALID_PARAM', { param: 'password', reason: 'not a string' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token != null && typeof token !== 'string') {
|
if (token != null && typeof token !== 'string') {
|
||||||
error(new ApiError('INVALID_PARAM', { param: 'token', reason: 'provided but not a string' }));
|
error('INVALID_PARAM', { param: 'token', reason: 'provided but not a string' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,19 +55,19 @@ export default async (ctx: Koa.Context) => {
|
||||||
}) as ILocalUser;
|
}) as ILocalUser;
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
error(new ApiError('NO_SUCH_USER'));
|
error('NO_SUCH_USER');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.isSuspended) {
|
if (user.isSuspended) {
|
||||||
error(new ApiError('SUSPENDED'));
|
error('SUSPENDED');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
|
|
||||||
// Compare password
|
// Compare password
|
||||||
const same = await bcrypt.compare(password, profile.password!);
|
const same = await comparePassword(password, profile.password!);
|
||||||
|
|
||||||
async function fail(): void {
|
async function fail(): void {
|
||||||
// Append signin history
|
// Append signin history
|
||||||
|
@ -92,7 +80,7 @@ export default async (ctx: Koa.Context) => {
|
||||||
success: false,
|
success: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
error(new ApiError('ACCESS_DENIED'));
|
error('ACCESS_DENIED');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!profile.twoFactorEnabled) {
|
if (!profile.twoFactorEnabled) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Koa from 'koa';
|
import Koa from 'koa';
|
||||||
import bcrypt from 'bcryptjs';
|
|
||||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha.js';
|
import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha.js';
|
||||||
|
import { hashPassword } from '@/misc/password.js';
|
||||||
import { Users, RegistrationTickets, UserPendings } from '@/models/index.js';
|
import { Users, RegistrationTickets, UserPendings } from '@/models/index.js';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import { sendEmail } from '@/services/send-email.js';
|
import { sendEmail } from '@/services/send-email.js';
|
||||||
|
@ -71,17 +71,13 @@ export default async (ctx: Koa.Context) => {
|
||||||
if (instance.emailRequiredForSignup) {
|
if (instance.emailRequiredForSignup) {
|
||||||
const code = secureRndstr(16);
|
const code = secureRndstr(16);
|
||||||
|
|
||||||
// Generate hash of password
|
|
||||||
const salt = await bcrypt.genSalt(8);
|
|
||||||
const hash = await bcrypt.hash(password, salt);
|
|
||||||
|
|
||||||
await UserPendings.insert({
|
await UserPendings.insert({
|
||||||
id: genId(),
|
id: genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
code,
|
code,
|
||||||
email: emailAddress,
|
email: emailAddress,
|
||||||
username,
|
username,
|
||||||
password: hash,
|
password: await hashPassword(password),
|
||||||
});
|
});
|
||||||
|
|
||||||
const link = `${config.url}/signup-complete/${code}`;
|
const link = `${config.url}/signup-complete/${code}`;
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import * as nestedProperty from 'nested-property';
|
import * as nestedProperty from 'nested-property';
|
||||||
import { EntitySchema, Repository, LessThan, Between } from 'typeorm';
|
import { EntitySchema, Repository, LessThan, Between } from 'typeorm';
|
||||||
import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/prelude/time.js';
|
import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/prelude/time.js';
|
||||||
|
import { unique } from '@/prelude/array.js';
|
||||||
import { getChartInsertLock } from '@/misc/app-lock.js';
|
import { getChartInsertLock } from '@/misc/app-lock.js';
|
||||||
import { db } from '@/db/postgre.js';
|
import { db } from '@/db/postgre.js';
|
||||||
import Logger from '../logger.js';
|
import Logger from '../logger.js';
|
||||||
|
@ -56,8 +57,6 @@ const camelToSnake = (str: string): string => {
|
||||||
return str.replace(/([A-Z])/g, s => '_' + s.charAt(0).toLowerCase());
|
return str.replace(/([A-Z])/g, s => '_' + s.charAt(0).toLowerCase());
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeDuplicates = (array: any[]) => Array.from(new Set(array));
|
|
||||||
|
|
||||||
type Commit<S extends Schema> = {
|
type Commit<S extends Schema> = {
|
||||||
[K in keyof S]?: S[K]['uniqueIncrement'] extends true ? string[] : number;
|
[K in keyof S]?: S[K]['uniqueIncrement'] extends true ? string[] : number;
|
||||||
};
|
};
|
||||||
|
@ -483,7 +482,7 @@ export default abstract class Chart<T extends Schema> {
|
||||||
this.buffer = this.buffer.filter(q => q.group != null && (q.group !== logHour.group));
|
this.buffer = this.buffer.filter(q => q.group != null && (q.group !== logHour.group));
|
||||||
};
|
};
|
||||||
|
|
||||||
const groups = removeDuplicates(this.buffer.map(log => log.group));
|
const groups = unique(this.buffer.map(log => log.group));
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
groups.map(group =>
|
groups.map(group =>
|
||||||
|
@ -651,12 +650,7 @@ export default abstract class Chart<T extends Schema> {
|
||||||
|
|
||||||
const res = {} as ChartResult<T>;
|
const res = {} as ChartResult<T>;
|
||||||
|
|
||||||
/**
|
// Turn array of objects into object of arrays.
|
||||||
* Turn
|
|
||||||
* [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }]
|
|
||||||
* into
|
|
||||||
* { foo: [1, 2, 3], bar: [5, 6, 7] }
|
|
||||||
*/
|
|
||||||
for (const record of chart) {
|
for (const record of chart) {
|
||||||
for (const [k, v] of Object.entries(record) as ([keyof typeof record, number])[]) {
|
for (const [k, v] of Object.entries(record) as ([keyof typeof record, number])[]) {
|
||||||
if (res[k]) {
|
if (res[k]) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import bcrypt from 'bcryptjs';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
|
import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
|
||||||
|
import { hashPassword } from '@/misc/password.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
import { UserProfile } from '@/models/entities/user-profile.js';
|
import { UserProfile } from '@/models/entities/user-profile.js';
|
||||||
import { genId } from '@/misc/gen-id.js';
|
import { genId } from '@/misc/gen-id.js';
|
||||||
|
@ -11,11 +11,7 @@ import { db } from '@/db/postgre.js';
|
||||||
import generateNativeUserToken from '@/server/api/common/generate-native-user-token.js';
|
import generateNativeUserToken from '@/server/api/common/generate-native-user-token.js';
|
||||||
|
|
||||||
export async function createSystemUser(username: string): Promise<User> {
|
export async function createSystemUser(username: string): Promise<User> {
|
||||||
const password = uuid();
|
const password = await hashPassword(uuid());
|
||||||
|
|
||||||
// Generate hash of password
|
|
||||||
const salt = await bcrypt.genSalt(8);
|
|
||||||
const hash = await bcrypt.hash(password, salt);
|
|
||||||
|
|
||||||
// Generate secret
|
// Generate secret
|
||||||
const secret = generateNativeUserToken();
|
const secret = generateNativeUserToken();
|
||||||
|
@ -55,7 +51,7 @@ export async function createSystemUser(username: string): Promise<User> {
|
||||||
await transactionalEntityManager.insert(UserProfile, {
|
await transactionalEntityManager.insert(UserProfile, {
|
||||||
userId: account.id,
|
userId: account.id,
|
||||||
autoAcceptFollowed: false,
|
autoAcceptFollowed: false,
|
||||||
password: hash,
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
await transactionalEntityManager.insert(UsedUsername, {
|
await transactionalEntityManager.insert(UsedUsername, {
|
||||||
|
|
|
@ -372,7 +372,7 @@ export async function addFile({
|
||||||
|
|
||||||
//#region Check drive usage
|
//#region Check drive usage
|
||||||
if (user && !isLink) {
|
if (user && !isLink) {
|
||||||
const usage = await DriveFiles.calcDriveUsageOf(user);
|
const usage = await DriveFiles.calcDriveUsageOf(user.id);
|
||||||
|
|
||||||
const instance = await fetchMeta();
|
const instance = await fetchMeta();
|
||||||
const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
|
const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { getActiveWebhooks } from '@/misc/webhook-cache.js';
|
||||||
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js';
|
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js';
|
||||||
import Logger from '../logger.js';
|
import Logger from '../logger.js';
|
||||||
import { createNotification } from '../create-notification.js';
|
import { createNotification } from '../create-notification.js';
|
||||||
import createFollowRequest from './requests/create.js';
|
import { createFollowRequest } from './requests/create.js';
|
||||||
|
|
||||||
const logger = new Logger('following/create');
|
const logger = new Logger('following/create');
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ type Both = Local | Remote;
|
||||||
/**
|
/**
|
||||||
* API following/request/reject
|
* API following/request/reject
|
||||||
*/
|
*/
|
||||||
export async function rejectFollowRequest(user: Local, follower: Both) {
|
export async function rejectFollowRequest(user: Local, follower: Both): Promise<void> {
|
||||||
if (Users.isRemoteUser(follower)) {
|
if (Users.isRemoteUser(follower)) {
|
||||||
deliverReject(user, follower);
|
deliverReject(user, follower);
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ export async function rejectFollowRequest(user: Local, follower: Both) {
|
||||||
/**
|
/**
|
||||||
* API following/reject
|
* API following/reject
|
||||||
*/
|
*/
|
||||||
export async function rejectFollow(user: Local, follower: Both) {
|
export async function rejectFollow(user: Local, follower: Both): Promise<void> {
|
||||||
if (Users.isRemoteUser(follower)) {
|
if (Users.isRemoteUser(follower)) {
|
||||||
deliverReject(user, follower);
|
deliverReject(user, follower);
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ export async function rejectFollow(user: Local, follower: Both) {
|
||||||
/**
|
/**
|
||||||
* AP Reject/Follow
|
* AP Reject/Follow
|
||||||
*/
|
*/
|
||||||
export async function remoteReject(actor: Remote, follower: Local) {
|
export async function remoteReject(actor: Remote, follower: Local): Promise<void> {
|
||||||
await removeFollowRequest(actor, follower);
|
await removeFollowRequest(actor, follower);
|
||||||
await removeFollow(actor, follower);
|
await removeFollow(actor, follower);
|
||||||
publishUnfollow(actor, follower);
|
publishUnfollow(actor, follower);
|
||||||
|
@ -63,7 +63,7 @@ export async function remoteReject(actor: Remote, follower: Local) {
|
||||||
/**
|
/**
|
||||||
* Remove follow request record
|
* Remove follow request record
|
||||||
*/
|
*/
|
||||||
async function removeFollowRequest(followee: Both, follower: Both) {
|
async function removeFollowRequest(followee: Both, follower: Both): Promise<void> {
|
||||||
const request = await FollowRequests.findOneBy({
|
const request = await FollowRequests.findOneBy({
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
|
@ -77,7 +77,7 @@ async function removeFollowRequest(followee: Both, follower: Both) {
|
||||||
/**
|
/**
|
||||||
* Remove follow record
|
* Remove follow record
|
||||||
*/
|
*/
|
||||||
async function removeFollow(followee: Both, follower: Both) {
|
async function removeFollow(followee: Both, follower: Both): Promise<void> {
|
||||||
const following = await Followings.findOneBy({
|
const following = await Followings.findOneBy({
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
|
@ -92,7 +92,7 @@ async function removeFollow(followee: Both, follower: Both) {
|
||||||
/**
|
/**
|
||||||
* Deliver Reject to remote
|
* Deliver Reject to remote
|
||||||
*/
|
*/
|
||||||
async function deliverReject(followee: Local, follower: Remote) {
|
async function deliverReject(followee: Local, follower: Remote): Promise<void> {
|
||||||
const request = await FollowRequests.findOneBy({
|
const request = await FollowRequests.findOneBy({
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
|
@ -105,7 +105,7 @@ async function deliverReject(followee: Local, follower: Remote) {
|
||||||
/**
|
/**
|
||||||
* Publish unfollow to local
|
* Publish unfollow to local
|
||||||
*/
|
*/
|
||||||
async function publishUnfollow(followee: Both, follower: Local) {
|
async function publishUnfollow(followee: Both, follower: Local): Promise<void> {
|
||||||
const packedFollowee = await Users.pack(followee.id, follower, {
|
const packedFollowee = await Users.pack(followee.id, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
import { FollowRequests, Users } from '@/models/index.js';
|
import { FollowRequests, Users } from '@/models/index.js';
|
||||||
import accept from './accept.js';
|
import { acceptFollowRequest } from './accept.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 指定したユーザー宛てのフォローリクエストをすべて承認
|
* Approve all follow requests addressed to the specified user.
|
||||||
* @param user ユーザー
|
* @param user The user whom to accept all follow requests to
|
||||||
*/
|
*/
|
||||||
export default async function(user: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }) {
|
export async function acceptAllFollowRequests(user: User): Promise<void> {
|
||||||
const requests = await FollowRequests.findBy({
|
const requests = await FollowRequests.findBy({
|
||||||
followeeId: user.id,
|
followeeId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const request of requests) {
|
for (const request of requests) {
|
||||||
const follower = await Users.findOneByOrFail({ id: request.followerId });
|
const follower = await Users.findOneByOrFail({ id: request.followerId });
|
||||||
accept(user, follower);
|
acceptFollowRequest(user, follower);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,17 @@ import renderFollow from '@/remote/activitypub/renderer/follow.js';
|
||||||
import renderAccept from '@/remote/activitypub/renderer/accept.js';
|
import renderAccept from '@/remote/activitypub/renderer/accept.js';
|
||||||
import { deliver } from '@/queue/index.js';
|
import { deliver } from '@/queue/index.js';
|
||||||
import { publishMainStream } from '@/services/stream.js';
|
import { publishMainStream } from '@/services/stream.js';
|
||||||
import { User, CacheableUser } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
import { FollowRequests, Users } from '@/models/index.js';
|
import { FollowRequests, Users } from '@/models/index.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { insertFollowingDoc } from '../create.js';
|
import { insertFollowingDoc } from '../create.js';
|
||||||
|
|
||||||
export default async function(followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, follower: CacheableUser) {
|
/**
|
||||||
|
* Accept a follow request from user `followee` to follow `follower`.
|
||||||
|
* @param followee User who is being followed
|
||||||
|
* @param follower User making the follow request
|
||||||
|
*/
|
||||||
|
export async function acceptFollowRequest(followee: User, follower: User): Promise<void> {
|
||||||
const request = await FollowRequests.findOneBy({
|
const request = await FollowRequests.findOneBy({
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
|
|
|
@ -7,7 +7,12 @@ import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
import { Users, FollowRequests } from '@/models/index.js';
|
import { Users, FollowRequests } from '@/models/index.js';
|
||||||
|
|
||||||
export default async function(followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox'] }, follower: { id: User['id']; host: User['host']; uri: User['host'] }) {
|
/**
|
||||||
|
* Cancel a follow request from `follower` to `followee`.
|
||||||
|
* @param followee User that was going to be followed
|
||||||
|
* @param follower User who is making the follow request
|
||||||
|
*/
|
||||||
|
export async function cancelFollowRequest(followee: User, follower: User): Promise<void> {
|
||||||
if (Users.isRemoteUser(followee)) {
|
if (Users.isRemoteUser(followee)) {
|
||||||
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
|
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,13 @@ import { Blockings, FollowRequests, Users } from '@/models/index.js';
|
||||||
import { genId } from '@/misc/gen-id.js';
|
import { genId } from '@/misc/gen-id.js';
|
||||||
import { createNotification } from '@/services/create-notification.js';
|
import { createNotification } from '@/services/create-notification.js';
|
||||||
|
|
||||||
export default async function(follower: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, followee: { id: User['id']; host: User['host']; uri: User['host']; inbox: User['inbox']; sharedInbox: User['sharedInbox']; }, requestId?: string) {
|
/**
|
||||||
|
* Make a follow request from `follower` to `followee`.
|
||||||
|
* @param follower User making the follow request
|
||||||
|
* @param followee User to make the follow request to
|
||||||
|
* @param requestId Follow request ID
|
||||||
|
*/
|
||||||
|
export async function createFollowRequest(follower: User, followee: User, requestId?: string): Promise<void> {
|
||||||
if (follower.id === followee.id) return;
|
if (follower.id === followee.id) return;
|
||||||
|
|
||||||
// check blocking
|
// check blocking
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<div style="margin-bottom: 16px;"><b>{{ i18n.ts.permission }}</b></div>
|
<div style="margin-bottom: 16px;"><b>{{ i18n.ts.permission }}</b></div>
|
||||||
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
|
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
|
||||||
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
|
||||||
<FormSwitch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ i18n.t(`_permissions.${kind}`) }}</FormSwitch>
|
<FormSwitch v-for="kind in kinds" :key="kind" v-model="permissions[kind]">{{ i18n.t(`_permissions.${kind}`) }}</FormSwitch>
|
||||||
</div>
|
</div>
|
||||||
</XModalWindow>
|
</XModalWindow>
|
||||||
</template>
|
</template>
|
||||||
|
@ -57,16 +57,15 @@ const emit = defineEmits<{
|
||||||
let dialog: InstanceType<typeof XModalWindow> | null = $ref(null);
|
let dialog: InstanceType<typeof XModalWindow> | null = $ref(null);
|
||||||
let name = $ref(props.initialName);
|
let name = $ref(props.initialName);
|
||||||
let perms: Record<string, boolean> = $ref({});
|
let perms: Record<string, boolean> = $ref({});
|
||||||
const kinds = $ref(permissions);
|
let kinds = props.initialPermissions.length > 0
|
||||||
|
? props.initialPermissions
|
||||||
|
: permissions;
|
||||||
|
|
||||||
if (props.initialPermissions.length > 0) {
|
// If there is a particular set of permissions given, enable all of them.
|
||||||
for (const kind of props.initialPermissions) {
|
// Otherwise, by default disable all permissions.
|
||||||
perms[kind] = true;
|
const enable = props.initialPermissions.length > 0;
|
||||||
}
|
for (const kind of kinds) {
|
||||||
} else {
|
perms[kind] = enable;
|
||||||
for (const kind of kinds) {
|
|
||||||
perms[kind] = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ok(): void {
|
function ok(): void {
|
||||||
|
|
|
@ -1,31 +1,83 @@
|
||||||
// TODO: useTooltip関数使うようにしたい
|
// TODO: use the useTooltip function
|
||||||
// ただディレクティブ内でonUnmountedなどのcomposition api使えるのか不明
|
|
||||||
|
|
||||||
import { defineAsyncComponent, Directive, ref } from 'vue';
|
import { defineAsyncComponent, Directive, ref } from 'vue';
|
||||||
import { isTouchUsing } from '@/scripts/touch';
|
import { isTouchUsing } from '@/scripts/touch';
|
||||||
import { popup, alert } from '@/os';
|
import { popup, alert } from '@/os';
|
||||||
|
|
||||||
const start = isTouchUsing ? 'touchstart' : 'mouseover';
|
|
||||||
const end = isTouchUsing ? 'touchend' : 'mouseleave';
|
|
||||||
const delay = 100;
|
const delay = 100;
|
||||||
|
|
||||||
|
class TooltipDirective {
|
||||||
|
public text: string | null;
|
||||||
|
private asMfm: boolean;
|
||||||
|
|
||||||
|
private _close: null | () => void;
|
||||||
|
private showTimer: null | ReturnType<typeof window.setTimeout>;
|
||||||
|
private hideTimer: null | ReturnType<typeof window.setTimeout>;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(binding) {
|
||||||
|
this.text = binding.value;
|
||||||
|
this.asMfm = binding.modifiers.mfm ?? false;
|
||||||
|
this._close = null;
|
||||||
|
this.showTimer = null;
|
||||||
|
this.hideTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private close(): void {
|
||||||
|
if (this.hideTimer != null) return; // already closed or closing
|
||||||
|
|
||||||
|
// cancel any pending attempts to show
|
||||||
|
window.clearTimeout(this.showTimer);
|
||||||
|
this.showTimer = null;
|
||||||
|
|
||||||
|
this.hideTimer = window.setTimeout(() => {
|
||||||
|
this._close?.();
|
||||||
|
this._close = null;
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
public show(el): void {
|
||||||
|
if (!document.body.contains(el)) return;
|
||||||
|
if (this.text == null) return; // no content
|
||||||
|
if (this.showTimer != null) return; // already showing or going to show
|
||||||
|
|
||||||
|
// cancel any pending attempts to hide
|
||||||
|
window.clearTimeout(this.hideTimer);
|
||||||
|
this.hideTimer = null;
|
||||||
|
|
||||||
|
this.showTimer = window.setTimeout(() => {
|
||||||
|
const showing = ref(true);
|
||||||
|
popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), {
|
||||||
|
showing,
|
||||||
|
text: this.text,
|
||||||
|
asMfm: this.asMfm,
|
||||||
|
targetElement: el,
|
||||||
|
}, {}, 'closed');
|
||||||
|
|
||||||
|
this._close = () => {
|
||||||
|
showing.value = false;
|
||||||
|
};
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a tooltip on mouseover. The content of the tooltip is the text
|
||||||
|
* provided as the value of this directive.
|
||||||
|
*
|
||||||
|
* Supported arguments:
|
||||||
|
* v-tooltip:dialog -> show text as a dialog on mousedown
|
||||||
|
*
|
||||||
|
* Supported modifiers:
|
||||||
|
* v-tooltip.mfm -> show tooltip content as MFM
|
||||||
|
*/
|
||||||
export default {
|
export default {
|
||||||
|
created(el: HTMLElement, binding) {
|
||||||
|
(el as any)._tooltipDirective_ = new TooltipDirective(binding);
|
||||||
|
},
|
||||||
|
|
||||||
mounted(el: HTMLElement, binding) {
|
mounted(el: HTMLElement, binding) {
|
||||||
const self = (el as any)._tooltipDirective_ = {} as any;
|
const self = el._tooltipDirective_ as TooltipDirective;
|
||||||
|
|
||||||
self.text = binding.value as string;
|
|
||||||
self._close = null;
|
|
||||||
self.showTimer = null;
|
|
||||||
self.hideTimer = null;
|
|
||||||
self.checkTimer = null;
|
|
||||||
|
|
||||||
self.close = () => {
|
|
||||||
if (self._close) {
|
|
||||||
window.clearInterval(self.checkTimer);
|
|
||||||
self._close();
|
|
||||||
self._close = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (binding.arg === 'dialog') {
|
if (binding.arg === 'dialog') {
|
||||||
el.addEventListener('click', (ev) => {
|
el.addEventListener('click', (ev) => {
|
||||||
|
@ -39,53 +91,20 @@ export default {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.show = () => {
|
// add event listeners
|
||||||
if (!document.body.contains(el)) return;
|
const start = isTouchUsing ? 'touchstart' : 'mouseover';
|
||||||
if (self._close) return;
|
const end = isTouchUsing ? 'touchend' : 'mouseleave';
|
||||||
if (self.text == null) return;
|
el.addEventListener(start, () => self.show(el), { passive: true });
|
||||||
|
el.addEventListener(end, () => self.close(), { passive: true });
|
||||||
const showing = ref(true);
|
el.addEventListener('click', self.close());
|
||||||
popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), {
|
el.addEventListener('selectstart', ev => ev.preventDefault());
|
||||||
showing,
|
|
||||||
text: self.text,
|
|
||||||
asMfm: binding.modifiers.mfm,
|
|
||||||
targetElement: el,
|
|
||||||
}, {}, 'closed');
|
|
||||||
|
|
||||||
self._close = () => {
|
|
||||||
showing.value = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
el.addEventListener('selectstart', ev => {
|
|
||||||
ev.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
el.addEventListener(start, () => {
|
|
||||||
window.clearTimeout(self.showTimer);
|
|
||||||
window.clearTimeout(self.hideTimer);
|
|
||||||
self.showTimer = window.setTimeout(self.show, delay);
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
el.addEventListener(end, () => {
|
|
||||||
window.clearTimeout(self.showTimer);
|
|
||||||
window.clearTimeout(self.hideTimer);
|
|
||||||
self.hideTimer = window.setTimeout(self.close, delay);
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
el.addEventListener('click', () => {
|
|
||||||
window.clearTimeout(self.showTimer);
|
|
||||||
self.close();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updated(el, binding) {
|
beforeUpdate(el, binding) {
|
||||||
const self = el._tooltipDirective_;
|
(el._tooltipDirective_ as TooltipDirective).text = binding.value as string;
|
||||||
self.text = binding.value as string;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
unmounted(el) {
|
beforeUnmount(el) {
|
||||||
const self = el._tooltipDirective_;
|
(el._tooltipDirective_ as TooltipDirective).close();
|
||||||
window.clearInterval(self.checkTimer);
|
|
||||||
},
|
},
|
||||||
} as Directive;
|
} as Directive;
|
||||||
|
|
|
@ -11,19 +11,25 @@
|
||||||
<div v-for="token in items" :key="token.id" class="_panel bfomjevm">
|
<div v-for="token in items" :key="token.id" class="_panel bfomjevm">
|
||||||
<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/>
|
<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<div class="name">{{ token.name }}</div>
|
<button class="_button" @click="revoke(token)"><i class="fas fa-trash-alt"></i></button>
|
||||||
<div class="description">{{ token.description }}</div>
|
<table>
|
||||||
<div class="_keyValue">
|
<tr>
|
||||||
<div>{{ i18n.ts.installedDate }}:</div>
|
<th>{{ i18n.ts.name }}:</th>
|
||||||
<div><MkTime :time="token.createdAt"/></div>
|
<td>{{ token.name }}</td>
|
||||||
</div>
|
</tr>
|
||||||
<div class="_keyValue">
|
<tr>
|
||||||
<div>{{ i18n.ts.lastUsedDate }}:</div>
|
<th>{{ i18n.ts.description }}:</th>
|
||||||
<div><MkTime :time="token.lastUsedAt"/></div>
|
<td>{{ token.description }}</td>
|
||||||
</div>
|
</tr>
|
||||||
<div class="actions">
|
<tr>
|
||||||
<button class="_button" @click="revoke(token)"><i class="fas fa-trash-alt"></i></button>
|
<th>{{ i18n.ts.installedDate }}:</th>
|
||||||
</div>
|
<td><MkTime :time="token.createdAt"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>{{ i18n.ts.lastUsedDate }}:</th>
|
||||||
|
<td><MkTime :time="token.lastUsedAt"/></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
<details>
|
<details>
|
||||||
<summary>{{ i18n.ts.details }}</summary>
|
<summary>{{ i18n.ts.details }}</summary>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -82,11 +88,19 @@ definePageMetadata({
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
width: calc(100% - 62px);
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
> .name {
|
button {
|
||||||
font-weight: bold;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -390,14 +390,6 @@ hr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
._keyValue {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
._link {
|
._link {
|
||||||
color: var(--link);
|
color: var(--link);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue