mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
feat(admin): add self-host setup and user management page (#7537)
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
Throttle,
|
||||
URLHelper,
|
||||
} from '../../fundamentals';
|
||||
import { Admin } from '../common';
|
||||
import { UserService } from '../user';
|
||||
import { UserType } from '../user/types';
|
||||
import { validators } from '../utils/validators';
|
||||
@@ -291,4 +292,19 @@ export class AuthResolver {
|
||||
|
||||
return emailVerifiedAt !== null;
|
||||
}
|
||||
|
||||
@Admin()
|
||||
@Mutation(() => String, {
|
||||
description: 'Create change password url',
|
||||
})
|
||||
async createChangePasswordUrl(
|
||||
@Args('userId') userId: string,
|
||||
@Args('callbackUrl') callbackUrl: string
|
||||
): Promise<string> {
|
||||
const token = await this.token.createToken(
|
||||
TokenType.ChangePassword,
|
||||
userId
|
||||
);
|
||||
return this.url.link(callbackUrl, { token });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ export class FeatureManagementService {
|
||||
return this.feature.addUserFeature(userId, FeatureType.Admin, 'Admin user');
|
||||
}
|
||||
|
||||
removeAdmin(userId: string) {
|
||||
return this.feature.removeUserFeature(userId, FeatureType.Admin);
|
||||
}
|
||||
|
||||
// ======== Early Access ========
|
||||
async addEarlyAccess(
|
||||
userId: string,
|
||||
|
||||
@@ -57,12 +57,15 @@ export class FeatureManagementResolver {
|
||||
|
||||
@Admin()
|
||||
@Mutation(() => Int)
|
||||
async removeEarlyAccess(@Args('email') email: string): Promise<number> {
|
||||
async removeEarlyAccess(
|
||||
@Args('email') email: string,
|
||||
@Args({ name: 'type', type: () => EarlyAccessType }) type: EarlyAccessType
|
||||
): Promise<number> {
|
||||
const user = await this.users.findUserByEmail(email);
|
||||
if (!user) {
|
||||
throw new UserNotFound();
|
||||
}
|
||||
return this.feature.removeEarlyAccess(user.id);
|
||||
return this.feature.removeEarlyAccess(user.id, type);
|
||||
}
|
||||
|
||||
@Admin()
|
||||
@@ -90,4 +93,18 @@ export class FeatureManagementResolver {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Admin()
|
||||
@Mutation(() => Boolean)
|
||||
async removeAdminister(@Args('email') email: string): Promise<boolean> {
|
||||
const user = await this.users.findUserByEmail(email);
|
||||
|
||||
if (!user) {
|
||||
throw new UserNotFound();
|
||||
}
|
||||
|
||||
await this.feature.removeAdmin(user.id);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { validators } from '../utils/validators';
|
||||
import { UserService } from './service';
|
||||
import {
|
||||
DeleteAccount,
|
||||
ManageUserInput,
|
||||
RemoveAvatar,
|
||||
UpdateUserInput,
|
||||
UserOrLimitedUser,
|
||||
@@ -174,6 +175,13 @@ export class UserManagementResolver {
|
||||
private readonly user: UserService
|
||||
) {}
|
||||
|
||||
@Query(() => Int, {
|
||||
description: 'Get users count',
|
||||
})
|
||||
async usersCount(): Promise<number> {
|
||||
return this.db.user.count();
|
||||
}
|
||||
|
||||
@Query(() => [UserType], {
|
||||
description: 'List registered users',
|
||||
})
|
||||
@@ -208,6 +216,26 @@ export class UserManagementResolver {
|
||||
return sessionUser(user);
|
||||
}
|
||||
|
||||
@Query(() => UserType, {
|
||||
name: 'userByEmail',
|
||||
description: 'Get user by email for admin',
|
||||
nullable: true,
|
||||
})
|
||||
async getUserByEmail(@Args('email') email: string) {
|
||||
const user = await this.db.user.findUnique({
|
||||
select: { ...this.user.defaultUserSelect, password: true },
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sessionUser(user);
|
||||
}
|
||||
|
||||
@Mutation(() => UserType, {
|
||||
description: 'Create a new user',
|
||||
})
|
||||
@@ -231,4 +259,36 @@ export class UserManagementResolver {
|
||||
await this.user.deleteUser(id);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@Mutation(() => UserType, {
|
||||
description: 'Update a user',
|
||||
})
|
||||
async updateUser(
|
||||
@Args('id') id: string,
|
||||
@Args('input') input: ManageUserInput
|
||||
): Promise<UserType> {
|
||||
const user = await this.db.user.findUnique({
|
||||
select: { ...this.user.defaultUserSelect, password: true },
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new UserNotFound();
|
||||
}
|
||||
validators.assertValidEmail(input.email);
|
||||
if (input.email !== user.email) {
|
||||
const exists = await this.db.user.findFirst({
|
||||
where: { email: input.email },
|
||||
});
|
||||
if (exists) {
|
||||
throw new Error('Email already exists');
|
||||
}
|
||||
}
|
||||
return sessionUser(
|
||||
await this.user.updateUser(user.id, {
|
||||
name: input.name,
|
||||
email: input.email,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,15 @@ export class UpdateUserInput implements Partial<User> {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class ManageUserInput {
|
||||
@Field({ description: 'User name', nullable: true })
|
||||
name?: string;
|
||||
|
||||
@Field({ description: 'User email' })
|
||||
email!: string;
|
||||
}
|
||||
|
||||
declare module '../../fundamentals/event/def' {
|
||||
interface UserEvents {
|
||||
admin: {
|
||||
|
||||
@@ -396,6 +396,14 @@ input ListUserInput {
|
||||
skip: Int = 0
|
||||
}
|
||||
|
||||
input ManageUserInput {
|
||||
"""User email"""
|
||||
email: String!
|
||||
|
||||
"""User name"""
|
||||
name: String
|
||||
}
|
||||
|
||||
type MissingOauthQueryParameterDataType {
|
||||
name: String!
|
||||
}
|
||||
@@ -412,6 +420,9 @@ type Mutation {
|
||||
"""Cleanup sessions"""
|
||||
cleanupCopilotSession(options: DeleteSessionInput!): [String!]!
|
||||
|
||||
"""Create change password url"""
|
||||
createChangePasswordUrl(callbackUrl: String!, userId: String!): String!
|
||||
|
||||
"""Create a subscription checkout link of stripe"""
|
||||
createCheckoutSession(input: CreateCheckoutSessionInput!): String!
|
||||
|
||||
@@ -445,10 +456,11 @@ type Mutation {
|
||||
leaveWorkspace(sendLeaveMail: Boolean, workspaceId: String!, workspaceName: String!): Boolean!
|
||||
publishPage(mode: PublicPageMode = Page, pageId: String!, workspaceId: String!): WorkspacePage!
|
||||
recoverDoc(guid: String!, timestamp: DateTime!, workspaceId: String!): DateTime!
|
||||
removeAdminister(email: String!): Boolean!
|
||||
|
||||
"""Remove user avatar"""
|
||||
removeAvatar: RemoveAvatar!
|
||||
removeEarlyAccess(email: String!): Int!
|
||||
removeEarlyAccess(email: String!, type: EarlyAccessType!): Int!
|
||||
removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Int!
|
||||
resumeSubscription(idempotencyKey: String!, plan: SubscriptionPlan = Pro): UserSubscription!
|
||||
revoke(userId: String!, workspaceId: String!): Boolean!
|
||||
@@ -474,6 +486,9 @@ type Mutation {
|
||||
updateRuntimeConfigs(updates: JSONObject!): [ServerRuntimeConfigType!]!
|
||||
updateSubscriptionRecurring(idempotencyKey: String!, plan: SubscriptionPlan = Pro, recurring: SubscriptionRecurring!): UserSubscription!
|
||||
|
||||
"""Update a user"""
|
||||
updateUser(id: String!, input: ManageUserInput!): UserType!
|
||||
|
||||
"""Update workspace"""
|
||||
updateWorkspace(input: UpdateWorkspaceInput!): WorkspaceType!
|
||||
|
||||
@@ -544,12 +559,18 @@ type Query {
|
||||
"""Get user by email"""
|
||||
user(email: String!): UserOrLimitedUser
|
||||
|
||||
"""Get user by email for admin"""
|
||||
userByEmail(email: String!): UserType
|
||||
|
||||
"""Get user by id"""
|
||||
userById(id: String!): UserType!
|
||||
|
||||
"""List registered users"""
|
||||
users(filter: ListUserInput!): [UserType!]!
|
||||
|
||||
"""Get users count"""
|
||||
usersCount: Int!
|
||||
|
||||
"""Get workspace by id"""
|
||||
workspace(id: String!): WorkspaceType!
|
||||
|
||||
|
||||
Reference in New Issue
Block a user