B2B-88: add starter kit structure and elements

This commit is contained in:
devmc-ee
2025-06-08 16:18:30 +03:00
parent 657a36a298
commit e7b25600cb
1280 changed files with 77893 additions and 5688 deletions

View File

@@ -0,0 +1,24 @@
import 'server-only';
import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@kit/supabase/database';
export function createAdminAccountsService(client: SupabaseClient<Database>) {
return new AdminAccountsService(client);
}
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;
}
}
}

View File

@@ -0,0 +1,203 @@
import 'server-only';
import { SupabaseClient } from '@supabase/supabase-js';
import { z } from 'zod';
import { Database } from '@kit/supabase/database';
export function createAdminAuthUserService(
client: SupabaseClient<Database>,
adminClient: SupabaseClient<Database>,
) {
return new AdminAuthUserService(client, adminClient);
}
/**
* @name AdminAuthUserService
* @description Service for performing admin actions on users in the system.
* This service only interacts with the Supabase Auth Admin API.
*/
class AdminAuthUserService {
constructor(
private readonly client: SupabaseClient<Database>,
private readonly adminClient: SupabaseClient<Database>,
) {}
/**
* Delete a user by deleting the user record and auth record.
* @param userId
*/
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.`);
}
}
/**
* Ban a user by setting the ban duration to `876600h` (100 years).
* @param userId
*/
async banUser(userId: string) {
await this.assertUserIsNotCurrentSuperAdmin(userId);
return this.setBanDuration(userId, `876600h`);
}
/**
* Reactivate a user by setting the ban duration to `none`.
* @param userId
*/
async reactivateUser(userId: string) {
await this.assertUserIsNotCurrentSuperAdmin(userId);
return this.setBanDuration(userId, `none`);
}
/**
* Impersonate a user by generating a magic link and returning the access and refresh tokens.
* @param userId
*/
async impersonateUser(userId: string) {
await this.assertUserIsNotCurrentSuperAdmin(userId);
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,
};
}
/**
* Assert that the target user is not the current user.
* @param targetUserId
*/
private 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`,
);
}
const targetUser =
await this.adminClient.auth.admin.getUserById(targetUserId);
const targetUserRole = targetUser.data.user?.app_metadata?.role;
if (targetUserRole === 'super-admin') {
throw new Error(
`You cannot perform a destructive action on a Super Admin account`,
);
}
}
private async setBanDuration(userId: string, banDuration: string) {
await this.adminClient.auth.admin.updateUserById(userId, {
ban_duration: banDuration,
});
}
/**
* Reset a user's password by sending a password reset email.
* @param userId
*/
async resetPassword(userId: string) {
await this.assertUserIsNotCurrentSuperAdmin(userId);
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 reset password`);
}
// Get the site URL from environment variable
const siteUrl = z.string().url().parse(process.env.NEXT_PUBLIC_SITE_URL);
const redirectTo = `${siteUrl}/update-password`;
const { error: resetError } =
await this.adminClient.auth.resetPasswordForEmail(email, {
redirectTo,
});
if (resetError) {
throw new Error(
`Error sending password reset email: ${resetError.message}`,
);
}
return {
success: true,
};
}
}

View File

@@ -0,0 +1,114 @@
import { SupabaseClient } from '@supabase/supabase-js';
import { getLogger } from '@kit/shared/logger';
import { Database } from '@kit/supabase/database';
export function createAdminDashboardService(client: SupabaseClient<Database>) {
return new AdminDashboardService(client);
}
export class AdminDashboardService {
constructor(private readonly client: SupabaseClient<Database>) {}
/**
* Get the dashboard data for the admin dashboard
* @param count
*/
async getDashboardData(
{ count }: { count: 'exact' | 'estimated' | 'planned' } = {
count: 'estimated',
},
) {
const logger = await getLogger();
const ctx = {
name: `admin.dashboard`,
};
const selectParams = {
count,
head: true,
};
const subscriptionsPromise = this.client
.from('subscriptions')
.select('*', selectParams)
.eq('status', 'active')
.then((response) => {
if (response.error) {
logger.error(
{ ...ctx, error: response.error.message },
`Error fetching active subscriptions`,
);
throw new Error();
}
return response.count;
});
const trialsPromise = this.client
.from('subscriptions')
.select('*', selectParams)
.eq('status', 'trialing')
.then((response) => {
if (response.error) {
logger.error(
{ ...ctx, error: response.error.message },
`Error fetching trialing subscriptions`,
);
throw new Error();
}
return response.count;
});
const accountsPromise = this.client
.from('accounts')
.select('*', selectParams)
.eq('is_personal_account', true)
.then((response) => {
if (response.error) {
logger.error(
{ ...ctx, error: response.error.message },
`Error fetching personal accounts`,
);
throw new Error();
}
return response.count;
});
const teamAccountsPromise = this.client
.from('accounts')
.select('*', selectParams)
.eq('is_personal_account', false)
.then((response) => {
if (response.error) {
logger.error(
{ ...ctx, error: response.error.message },
`Error fetching team accounts`,
);
throw new Error();
}
return response.count;
});
const [subscriptions, trials, accounts, teamAccounts] = await Promise.all([
subscriptionsPromise,
trialsPromise,
accountsPromise,
teamAccountsPromise,
]);
return {
subscriptions,
trials,
accounts,
teamAccounts,
};
}
}