Update dependencies and enhance admin account page
This commit updates various dependencies in pnpm-lock file and introduces enhancements to the admin account page. This includes adding several new functionality like 'Delete User', 'Ban User', 'Impersonate User' and 'Delete Account'. Various UI components are also added for these features.
This commit is contained in:
127
packages/features/admin/src/lib/server/admin-server-actions.ts
Normal file
127
packages/features/admin/src/lib/server/admin-server-actions.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
'use server';
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||
|
||||
import { enhanceAdminAction } from './enhance-admin-action';
|
||||
import {
|
||||
BanUserSchema,
|
||||
DeleteAccountSchema,
|
||||
DeleteUserSchema,
|
||||
ImpersonateUserSchema,
|
||||
ReactivateUserSchema,
|
||||
} from './schema/admin-actions.schema';
|
||||
import { AdminAccountsService } from './services/admin-accounts.service';
|
||||
import { AdminAuthUserService } from './services/admin-auth-user.service';
|
||||
|
||||
/**
|
||||
* @name banUser
|
||||
* @description Ban a user from the system.
|
||||
*/
|
||||
export const banUser = enhanceAdminAction(
|
||||
enhanceAction(
|
||||
async ({ userId }) => {
|
||||
const service = getAdminAuthService();
|
||||
|
||||
await service.banUser(userId);
|
||||
|
||||
revalidateAdmin();
|
||||
},
|
||||
{
|
||||
schema: BanUserSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* @name reactivateUser
|
||||
* @description Reactivate a user in the system.
|
||||
*/
|
||||
export const reactivateUser = enhanceAdminAction(
|
||||
enhanceAction(
|
||||
async ({ userId }) => {
|
||||
const service = getAdminAuthService();
|
||||
|
||||
await service.reactivateUser(userId);
|
||||
|
||||
revalidateAdmin();
|
||||
},
|
||||
{
|
||||
schema: ReactivateUserSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* @name impersonateUser
|
||||
* @description Impersonate a user in the system.
|
||||
*/
|
||||
export const impersonateUser = enhanceAdminAction(
|
||||
enhanceAction(
|
||||
async ({ userId }) => {
|
||||
const service = getAdminAuthService();
|
||||
|
||||
return await service.impersonateUser(userId);
|
||||
},
|
||||
{
|
||||
schema: ImpersonateUserSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* @name deleteUser
|
||||
* @description Delete a user from the system.
|
||||
*/
|
||||
export const deleteUser = enhanceAdminAction(
|
||||
enhanceAction(
|
||||
async ({ userId }) => {
|
||||
const service = getAdminAuthService();
|
||||
|
||||
await service.deleteUser(userId);
|
||||
|
||||
revalidateAdmin();
|
||||
},
|
||||
{
|
||||
schema: DeleteUserSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* @name deleteAccount
|
||||
* @description Delete an account from the system.
|
||||
*/
|
||||
export const deleteAccount = enhanceAdminAction(
|
||||
enhanceAction(
|
||||
async ({ accountId }) => {
|
||||
const service = getAdminAccountsService();
|
||||
|
||||
await service.deleteAccount(accountId);
|
||||
|
||||
revalidateAdmin();
|
||||
},
|
||||
{
|
||||
schema: DeleteAccountSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
function getAdminAuthService() {
|
||||
const client = getSupabaseServerActionClient();
|
||||
const adminClient = getSupabaseServerActionClient({ admin: true });
|
||||
|
||||
return new AdminAuthUserService(client, adminClient);
|
||||
}
|
||||
|
||||
function getAdminAccountsService() {
|
||||
const adminClient = getSupabaseServerActionClient({ admin: true });
|
||||
|
||||
return new AdminAccountsService(adminClient);
|
||||
}
|
||||
|
||||
function revalidateAdmin() {
|
||||
revalidatePath('/admin', 'layout');
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
||||
|
||||
import { isSuperAdmin } from './is-super-admin';
|
||||
|
||||
export function enhanceAdminAction<Args, Response>(
|
||||
fn: (params: Args) => Response,
|
||||
) {
|
||||
return async (params: Args) => {
|
||||
const isAdmin = await isSuperAdmin(getSupabaseServerActionClient());
|
||||
|
||||
if (!isAdmin) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return fn(params);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const confirmationSchema = z.object({
|
||||
confirmation: z.custom((value) => value === 'CONFIRM'),
|
||||
});
|
||||
|
||||
const UserIdSchema = confirmationSchema.extend({
|
||||
userId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export const BanUserSchema = UserIdSchema;
|
||||
export const ReactivateUserSchema = UserIdSchema;
|
||||
export const ImpersonateUserSchema = UserIdSchema;
|
||||
export const DeleteUserSchema = UserIdSchema;
|
||||
|
||||
export const DeleteAccountSchema = confirmationSchema.extend({
|
||||
accountId: z.string().uuid(),
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
export class AdminAccountsService {
|
||||
constructor(private adminClient: SupabaseClient<Database>) {}
|
||||
|
||||
async deleteAccount(accountId: string) {
|
||||
const { error } = await this.adminClient
|
||||
.from('accounts')
|
||||
.delete()
|
||||
.eq('id', accountId);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
/**
|
||||
* @name AdminAuthUserService
|
||||
* @description Service for performing admin actions on users in the system.
|
||||
* This service only interacts with the Supabase Auth Admin API.
|
||||
*/
|
||||
export class AdminAuthUserService {
|
||||
constructor(
|
||||
private readonly client: SupabaseClient<Database>,
|
||||
private readonly adminClient: SupabaseClient<Database>,
|
||||
) {}
|
||||
|
||||
async assertUserIsNotCurrentSuperAdmin(targetUserId: string) {
|
||||
const { data: user } = await this.client.auth.getUser();
|
||||
const currentUserId = user.user?.id;
|
||||
|
||||
if (!currentUserId) {
|
||||
throw new Error(`Error fetching user`);
|
||||
}
|
||||
|
||||
if (currentUserId === targetUserId) {
|
||||
throw new Error(
|
||||
`You cannot perform a destructive action on your own account as a Super Admin`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteUser(userId: string) {
|
||||
await this.assertUserIsNotCurrentSuperAdmin(userId);
|
||||
|
||||
const deleteUserResponse =
|
||||
await this.adminClient.auth.admin.deleteUser(userId);
|
||||
|
||||
if (deleteUserResponse.error) {
|
||||
throw new Error(`Error deleting user record or auth record.`);
|
||||
}
|
||||
}
|
||||
|
||||
async banUser(userId: string) {
|
||||
await this.assertUserIsNotCurrentSuperAdmin(userId);
|
||||
|
||||
return this.setBanDuration(userId, `876600h`);
|
||||
}
|
||||
|
||||
async reactivateUser(userId: string) {
|
||||
await this.assertUserIsNotCurrentSuperAdmin(userId);
|
||||
|
||||
return this.setBanDuration(userId, `none`);
|
||||
}
|
||||
|
||||
async impersonateUser(userId: string) {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await this.adminClient.auth.admin.getUserById(userId);
|
||||
|
||||
if (error ?? !user) {
|
||||
throw new Error(`Error fetching user`);
|
||||
}
|
||||
|
||||
const email = user.email;
|
||||
|
||||
if (!email) {
|
||||
throw new Error(`User has no email. Cannot impersonate`);
|
||||
}
|
||||
|
||||
const { error: linkError, data } =
|
||||
await this.adminClient.auth.admin.generateLink({
|
||||
type: 'magiclink',
|
||||
email,
|
||||
options: {
|
||||
redirectTo: `/`,
|
||||
},
|
||||
});
|
||||
|
||||
if (linkError ?? !data) {
|
||||
throw new Error(`Error generating magic link`);
|
||||
}
|
||||
|
||||
const response = await fetch(data.properties?.action_link, {
|
||||
method: 'GET',
|
||||
redirect: 'manual',
|
||||
});
|
||||
|
||||
const location = response.headers.get('Location');
|
||||
|
||||
if (!location) {
|
||||
throw new Error(`Error generating magic link. Location header not found`);
|
||||
}
|
||||
|
||||
const hash = new URL(location).hash.substring(1);
|
||||
const query = new URLSearchParams(hash);
|
||||
const accessToken = query.get('access_token');
|
||||
const refreshToken = query.get('refresh_token');
|
||||
|
||||
if (!accessToken || !refreshToken) {
|
||||
throw new Error(
|
||||
`Error generating magic link. Tokens not found in URL hash.`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
private async setBanDuration(userId: string, banDuration: string) {
|
||||
await this.adminClient.auth.admin.updateUserById(userId, {
|
||||
ban_duration: banDuration,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user