Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • TransFem-org/Sharkey
  • fEmber/Sharkey
  • kopper/Sharkey
  • dakkar/Sharkey-thenautilus
  • esm/Sharkey
  • blueb/Sharkey
  • aura/Sharkey
  • alicem/Sharkey
  • dimkr/Sharkey
  • vvinrg/Sharkey
  • tess/Sharkey
  • Latte_macchiato/Sharkey
  • skyevg/Sharkey
  • TransFem-org/sharkey-trans-fem
  • limepotato/sharkey
  • elrant/villkey
  • kio/kitsukey
  • 4censord/Sharkey
  • kanade/Sharkey
  • kakkokari-gtyih/Sharkey
  • piuvas/Sharkey
  • kio/gaykey
  • owlbear/Sharkey
  • esurio/Sharkey
  • cuteBoiButt/Sharkey
  • magi/Sharkey
  • chikorita157/Sharkey
  • CenTdemeern1/Sharkey
  • GeopJr/Sharkey
  • lhcfl/sharkey
  • cody/Sharkey
  • puppygirlhornypost/Sharkey
  • Sneexy/Sharkey
  • zotan/Sharkey
  • Marie/Sharkey
  • maciejla/Sharkey
  • transitory/Sharkey
  • Caramel/Sharkey
  • fly_mc/Sharkey
  • 87/Sharkey
  • echo/Sharkey
  • transfemsoc/Sharkey
  • ch0ccyra1n/Sharkey
  • interru/Sharkey
  • legiayayana/Sharkey
  • elizabeth-dev/Sharkey
  • JeDaYoshi/Sharkey
  • bunnybeam/Sharkey
  • and-then-there-were-two/Sharkey
  • easrng/Sharkey
  • HellhoundSoftware/Sharkey
  • realkinetix/Sharkey
  • sandervankasteel/Sharkey
  • jacobwhall/Sharkey
  • Daniel/Sharkey
  • rhythmkey/rhythmkey
56 results
Show changes
Commits on Source (14)
Showing
with 231 additions and 8 deletions
......@@ -638,6 +638,8 @@ userSuspended: "This user has been suspended."
userSilenced: "This user is being silenced."
yourAccountSuspendedTitle: "This account is suspended"
yourAccountSuspendedDescription: "This account has been suspended due to breaking the server's terms of services or similar. Contact the administrator if you would like to know a more detailed reason. Please do not create a new account."
systemAccountTitle: "This is a system account"
systemAccountDescription: "This account is created and managed automatically by the system, and cannot be logged into."
tokenRevoked: "Invalid token"
tokenRevokedDescription: "This token has expired. Please log in again."
accountDeleted: "Account deleted"
......
......@@ -2568,6 +2568,14 @@ export interface Locale extends ILocale {
* このアカウントは、サーバーの利用規約に違反したなどの理由により、凍結されています。詳細については管理者までお問い合わせください。新しいアカウントを作らないでください。
*/
"yourAccountSuspendedDescription": string;
/**
* This is a system account
*/
"systemAccountTitle": string;
/**
* This account is created and managed automatically by the system, and cannot be logged into.
*/
"systemAccountDescription": string;
/**
* トークンが無効です
*/
......
......@@ -638,6 +638,8 @@ userSuspended: "このユーザーは凍結されています。"
userSilenced: "このユーザーはサイレンスされています。"
yourAccountSuspendedTitle: "アカウントが凍結されています"
yourAccountSuspendedDescription: "このアカウントは、サーバーの利用規約に違反したなどの理由により、凍結されています。詳細については管理者までお問い合わせください。新しいアカウントを作らないでください。"
systemAccountTitle: "This is a system account"
systemAccountDescription: "This account is created and managed automatically by the system, and cannot be logged into."
tokenRevoked: "トークンが無効です"
tokenRevokedDescription: "ログイントークンが失効しています。ログインし直してください。"
accountDeleted: "アカウントは削除されています"
......
......@@ -13,6 +13,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { isSystemAccount } from '@/misc/is-system-account.js';
@Injectable()
export class DeleteAccountService {
......@@ -38,6 +39,7 @@ export class DeleteAccountService {
}, moderator?: MiUser): Promise<void> {
const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
if (_user.isRoot) throw new Error('cannot delete a root account');
if (isSystemAccount(_user)) throw new Error('cannot delete a system account');
if (moderator != null) {
this.moderationLogService.log(moderator, 'deleteAccount', {
......
......@@ -15,6 +15,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { RelationshipJobData } from '@/queue/types.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { isSystemAccount } from '@/misc/is-system-account.js';
@Injectable()
export class UserSuspendService {
......@@ -38,6 +39,8 @@ export class UserSuspendService {
@bindThis
public async suspend(user: MiUser, moderator: MiUser): Promise<void> {
if (isSystemAccount(user)) throw new Error('cannot suspend a system account');
await this.usersRepository.update(user.id, {
isSuspended: true,
});
......
......@@ -53,6 +53,7 @@ import type { OnModuleInit } from '@nestjs/common';
import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
import type { PageEntityService } from './PageEntityService.js';
import { isSystemAccount } from '@/misc/is-system-account.js';
const Ajv = _Ajv.default;
const ajv = new Ajv();
......@@ -614,6 +615,7 @@ export class UserEntityService implements OnModuleInit {
backgroundId: user.backgroundId,
isModerator: isModerator,
isAdmin: isAdmin,
isSystem: isSystemAccount(user),
injectFeaturedNote: profile!.injectFeaturedNote,
receiveAnnouncementEmail: profile!.receiveAnnouncementEmail,
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
......
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
interface UserLike {
readonly username: string;
readonly host: string | null;
}
/**
* Checks if the given user represents a system account, such as instance.actor.
*/
export function isSystemAccount(user: UserLike): boolean {
return user.host == null && user.username.includes('.');
}
......@@ -121,6 +121,11 @@ export const packedUserLiteSchema = {
nullable: false, optional: true,
default: false,
},
isSystem: {
type: 'boolean',
nullable: false, optional: true,
default: false,
},
isSilenced: {
type: 'boolean',
nullable: false, optional: false,
......
......@@ -26,6 +26,7 @@ import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { isSystemAccount } from '@/misc/is-system-account.js';
@Injectable()
export class SigninApiService {
......@@ -125,6 +126,12 @@ export class SigninApiService {
});
}
if (isSystemAccount(user)) {
return error(403, {
id: 's8dhsj9s-a93j-493j-ja9k-kas9sj20aml2',
});
}
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (!user.approved && instance.approvalRequiredForSignup) {
......
......@@ -11,6 +11,7 @@ import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { isSystemAccount } from '@/misc/is-system-account.js';
export const meta = {
tags: ['admin'],
......@@ -63,6 +64,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('cannot reset password of root');
}
if (isSystemAccount(user)) {
throw new Error('cannot reset password of system account');
}
const passwd = secureRndstr(8);
// Generate hash of password
......
......@@ -11,6 +11,7 @@ import { RoleService } from '@/core/RoleService.js';
import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
import { IdService } from '@/core/IdService.js';
import { notificationRecieveConfig } from '@/models/json-schema/user.js';
import { isSystemAccount } from '@/misc/is-system-account.js';
export const meta = {
tags: ['admin'],
......@@ -31,6 +32,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
approved: {
type: 'boolean',
optional: false, nullable: false,
},
autoAcceptFollowed: {
type: 'boolean',
optional: false, nullable: false,
......@@ -111,6 +116,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
isSystem: {
type: 'boolean',
optional: false, nullable: false,
},
isSilenced: {
type: 'boolean',
optional: false, nullable: false,
......@@ -240,6 +249,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
mutedInstances: profile.mutedInstances,
notificationRecieveConfig: profile.notificationRecieveConfig,
isModerator: isModerator,
isSystem: isSystemAccount(user),
isSilenced: isSilenced,
isSuspended: user.isSuspended,
isHibernated: user.isHibernated,
......
......@@ -33,6 +33,8 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
includeFollower: { type: 'boolean', default: false },
includeFollowee: { type: 'boolean', default: true },
},
required: ['host'],
} as const;
......@@ -54,7 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
return await this.followingEntityService.packMany(followings, me, { populateFollowee: true });
return await this.followingEntityService.packMany(followings, me, { populateFollowee: ps.includeFollowee, populateFollower: ps.includeFollower });
});
}
}
......@@ -33,6 +33,8 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
includeFollower: { type: 'boolean', default: false },
includeFollowee: { type: 'boolean', default: true },
},
required: ['host'],
} as const;
......@@ -54,7 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
return await this.followingEntityService.packMany(followings, me, { populateFollowee: true });
return await this.followingEntityService.packMany(followings, me, { populateFollowee: ps.includeFollowee, populateFollower: ps.includeFollower });
});
}
}
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { isSystemAccount } from '@/misc/is-system-account.js';
describe(isSystemAccount, () => {
it('should return true for instance.actor', () => {
expect(isSystemAccount({ username: 'instance.actor', host: null })).toBeTruthy();
});
it('should return true for relay.actor', () => {
expect(isSystemAccount({ username: 'relay.actor', host: null })).toBeTruthy();
});
it('should return true for any username with a dot', () => {
expect(isSystemAccount({ username: 'some.user', host: null })).toBeTruthy();
expect(isSystemAccount({ username: 'some.', host: null })).toBeTruthy();
expect(isSystemAccount({ username: '.user', host: null })).toBeTruthy();
expect(isSystemAccount({ username: '.', host: null })).toBeTruthy();
});
it('should return true for usernames with multiple dots', () => {
expect(isSystemAccount({ username: 'some.user.account', host: null })).toBeTruthy();
expect(isSystemAccount({ username: '..', host: null })).toBeTruthy();
});
it('should return false for usernames without a dot', () => {
expect(isSystemAccount({ username: 'instance_actor', host: null })).toBeFalsy();
expect(isSystemAccount({ username: 'instanceactor', host: null })).toBeFalsy();
expect(isSystemAccount({ username: 'relay_actor', host: null })).toBeFalsy();
expect(isSystemAccount({ username: 'relayactor', host: null })).toBeFalsy();
expect(isSystemAccount({ username: '', host: null })).toBeFalsy();
});
it('should return false for users from another instance', () => {
expect(isSystemAccount({ username: 'instance.actor', host: 'example.com' })).toBeFalsy();
expect(isSystemAccount({ username: 'relay.actor', host: 'example.com' })).toBeFalsy();
expect(isSystemAccount({ username: 'some.user', host: 'example.com' })).toBeFalsy();
expect(isSystemAccount({ username: 'some.', host: 'example.com' })).toBeFalsy();
expect(isSystemAccount({ username: '.user', host: 'example.com' })).toBeFalsy();
expect(isSystemAccount({ username: '.', host: 'example.com' })).toBeFalsy();
expect(isSystemAccount({ username: 'some.user.account', host: 'example.com' })).toBeFalsy();
expect(isSystemAccount({ username: '..', host: 'example.com' })).toBeFalsy();
});
});
......@@ -77,6 +77,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { query, extractDomain } from '@/scripts/url.js';
import { login } from '@/account.js';
import { i18n } from '@/i18n.js';
import { showSystemAccountDialog } from '@/scripts/show-system-account-dialog.js';
const signing = ref(false);
const user = ref<Misskey.entities.UserDetailed | null>(null);
......@@ -204,6 +205,10 @@ function loginFailed(err: any): void {
showSuspendedDialog();
break;
}
case 's8dhsj9s-a93j-493j-ja9k-kas9sj20aml2': {
showSystemAccountDialog();
break;
}
case '22d05606-fbcf-421a-a2db-b32610dcfd1b': {
os.alert({
type: 'error',
......
......@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<MkInfo v-if="['instance.actor', 'relay.actor'].includes(user.username)">{{ i18n.ts.isSystemAccount }}</MkInfo>
<MkInfo v-if="isSystem">{{ i18n.ts.isSystemAccount }}</MkInfo>
<FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ i18n.ts.instanceInfo }}</FormLink>
......@@ -79,11 +79,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection>
<div class="_gaps">
<MkSwitch v-model="silenced" @update:modelValue="toggleSilence">{{ i18n.ts.silence }}</MkSwitch>
<MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
<MkSwitch v-if="!isSystem" v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
<MkSwitch v-model="markedAsNSFW" @update:modelValue="toggleNSFW">{{ i18n.ts.markAsNSFW }}</MkSwitch>
<div>
<MkButton v-if="user.host == null" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
<MkButton v-if="user.host == null && !isSystem" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
</div>
<MkFolder>
......@@ -114,7 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="unsetUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.unsetUserBanner }}</MkButton>
<MkButton v-if="iAmModerator" inline danger @click="deleteAllFiles"><i class="ph-cloud ph-bold ph-lg"></i> {{ i18n.ts.deleteAllFiles }}</MkButton>
</div>
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
<MkButton v-if="$i.isAdmin && !isSystem" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
</div>
</FormSection>
</div>
......@@ -227,15 +227,16 @@ const tab = ref(props.initialTab);
const chartSrc = ref('per-user-notes');
const user = ref<null | Misskey.entities.UserDetailed>();
const init = ref<ReturnType<typeof createFetcher>>();
const info = ref<any>();
const info = ref<Misskey.entities.AdminShowUserResponse | null>(null);
const ips = ref<Misskey.entities.AdminGetUserIpsResponse | null>(null);
const ap = ref<any>(null);
const ap = ref<Misskey.entities.ApGetResponse | null>(null);
const moderator = ref(false);
const silenced = ref(false);
const approved = ref(false);
const suspended = ref(false);
const markedAsNSFW = ref(false);
const moderationNote = ref('');
const isSystem = computed(() => info.value?.isSystem ?? false);
const filesPagination = {
endpoint: 'admin/drive/files' as const,
limit: 10,
......
......@@ -131,6 +131,36 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
</MkPagination>
</div>
<div v-else-if="tab === 'following'" key="following" class="_gaps_m">
<MkPagination v-slot="{items}" :pagination="followingPagination">
<div class="follow-relations-list">
<div v-for="followRelationship in items" :key="followRelationship.id" class="follow-relation">
<MkA v-tooltip.mfm="`Last posted: ${dateString(followRelationship.followee.updatedAt)}`" :to="`/admin/user/${followRelationship.followee.id}`" class="user">
<MkUserCardMini :user="followRelationship.followee" :withChart="false"/>
</MkA>
<span class="arrow"></span>
<MkA v-tooltip.mfm="`Last posted: ${dateString(followRelationship.follower.updatedAt)}`" :to="`/admin/user/${followRelationship.follower.id}`" class="user">
<MkUserCardMini :user="followRelationship.follower" :withChart="false"/>
</MkA>
</div>
</div>
</MkPagination>
</div>
<div v-else-if="tab === 'followers'" key="followers" class="_gaps_m">
<MkPagination v-slot="{items}" :pagination="followersPagination">
<div class="follow-relations-list">
<div v-for="followRelationship in items" :key="followRelationship.id" class="follow-relation">
<MkA v-tooltip.mfm="`Last posted: ${dateString(followRelationship.followee.updatedAt)}`" :to="`/admin/user/${followRelationship.followee.id}`" class="user">
<MkUserCardMini :user="followRelationship.followee" :withChart="false"/>
</MkA>
<span class="arrow"></span>
<MkA v-tooltip.mfm="`Last posted: ${dateString(followRelationship.follower.updatedAt)}`" :to="`/admin/user/${followRelationship.follower.id}`" class="user">
<MkUserCardMini :user="followRelationship.follower" :withChart="false"/>
</MkA>
</div>
</div>
</MkPagination>
</div>
<div v-else-if="tab === 'raw'" key="raw" class="_gaps_m">
<MkObjectView tall :value="instance">
</MkObjectView>
......@@ -210,6 +240,26 @@ const usersPagination = {
offsetMode: true,
};
const followingPagination = {
endpoint: 'federation/following' as const,
limit: 10,
params: {
host: props.host,
includeFollower: true,
},
offsetMode: false,
};
const followersPagination = {
endpoint: 'federation/followers' as const,
limit: 10,
params: {
host: props.host,
includeFollower: true,
},
offsetMode: false,
};
watch(moderationNote, async () => {
await misskeyApi('admin/federation/update-instance', { host: instance.value.host, moderationNote: moderationNote.value });
});
......@@ -363,6 +413,14 @@ const headerTabs = computed(() => [{
key: 'users',
title: i18n.ts.users,
icon: 'ti ti-users',
}, {
key: 'following',
title: i18n.ts.following,
icon: 'ti ti-arrow-right',
}, {
key: 'followers',
title: i18n.ts.followers,
icon: 'ti ti-arrow-left',
}, {
key: 'raw',
title: 'Raw',
......@@ -405,4 +463,31 @@ definePageMetadata(() => ({
}
}
}
.follow-relations-list {
display: flex;
flex-direction: column;
gap: 12px;
.follow-relation {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: nowrap;
justify-content: space-between;
.user {
flex: 1;
max-width: 45%;
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.arrow {
font-size: 1.5em;
flex-shrink: 0;
}
}
}
</style>
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
export function showSystemAccountDialog(): Promise<void> {
return os.alert({
type: 'error',
title: i18n.ts.systemAccountTitle,
text: i18n.ts.systemAccountDescription,
});
}
......@@ -3836,6 +3836,8 @@ export type components = {
isAdmin?: boolean;
/** @default false */
isModerator?: boolean;
/** @default false */
isSystem?: boolean;
isSilenced: boolean;
noindex: boolean;
isBot?: boolean;
......@@ -9077,6 +9079,7 @@ export type operations = {
'application/json': {
email: string | null;
emailVerified: boolean;
approved: boolean;
autoAcceptFollowed: boolean;
noCrawle: boolean;
preventAiLearning: boolean;
......@@ -9216,6 +9219,7 @@ export type operations = {
}]>;
};
isModerator: boolean;
isSystem: boolean;
isSilenced: boolean;
isSuspended: boolean;
isHibernated: boolean;
......