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,131 @@
import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@kit/supabase/database';
/**
* Class representing an API for interacting with user accounts.
* @constructor
* @param {SupabaseClient<Database>} client - The Supabase client instance.
*/
class AccountsApi {
constructor(private readonly client: SupabaseClient<Database>) {}
/**
* @name getAccount
* @description Get the account data for the given ID.
* @param id
*/
async getAccount(id: string) {
const { data, error } = await this.client
.from('accounts')
.select('*')
.eq('id', id)
.single();
if (error) {
throw error;
}
return data;
}
/**
* @name getAccountWorkspace
* @description Get the account workspace data.
*/
async getAccountWorkspace() {
const { data, error } = await this.client
.from('user_account_workspace')
.select(`*`)
.single();
if (error) {
throw error;
}
return data;
}
/**
* @name loadUserAccounts
* Load the user accounts.
*/
async loadUserAccounts() {
const { data: accounts, error } = await this.client
.from('user_accounts')
.select(`name, slug, picture_url`);
if (error) {
throw error;
}
return accounts.map(({ name, slug, picture_url }) => {
return {
label: name,
value: slug,
image: picture_url,
};
});
}
/**
* @name getSubscription
* Get the subscription data for the given user.
* @param accountId
*/
async getSubscription(accountId: string) {
const response = await this.client
.from('subscriptions')
.select('*, items: subscription_items !inner (*)')
.eq('account_id', accountId)
.maybeSingle();
if (response.error) {
throw response.error;
}
return response.data;
}
/**
* Get the orders data for the given account.
* @param accountId
*/
async getOrder(accountId: string) {
const response = await this.client
.from('orders')
.select('*, items: order_items !inner (*)')
.eq('account_id', accountId)
.maybeSingle();
if (response.error) {
throw response.error;
}
return response.data;
}
/**
* @name getCustomerId
* Get the billing customer ID for the given user.
* If the user does not have a billing customer ID, it will return null.
* @param accountId
*/
async getCustomerId(accountId: string) {
const response = await this.client
.from('billing_customers')
.select('customer_id')
.eq('account_id', accountId)
.maybeSingle();
if (response.error) {
throw response.error;
}
return response.data?.customer_id;
}
}
export function createAccountsApi(client: SupabaseClient<Database>) {
return new AccountsApi(client);
}

View File

@@ -0,0 +1,104 @@
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { enhanceAction } from '@kit/next/actions';
import { createOtpApi } from '@kit/otp';
import { getLogger } from '@kit/shared/logger';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { DeletePersonalAccountSchema } from '../schema/delete-personal-account.schema';
import { createDeletePersonalAccountService } from './services/delete-personal-account.service';
const enableAccountDeletion =
process.env.NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION === 'true';
export async function refreshAuthSession() {
const client = getSupabaseServerClient();
await client.auth.refreshSession();
return {};
}
export const deletePersonalAccountAction = enhanceAction(
async (formData: FormData, user) => {
const logger = await getLogger();
// validate the form data
const { success } = DeletePersonalAccountSchema.safeParse(
Object.fromEntries(formData.entries()),
);
if (!success) {
throw new Error('Invalid form data');
}
const ctx = {
name: 'account.delete',
userId: user.id,
};
const otp = formData.get('otp') as string;
if (!otp) {
throw new Error('OTP is required');
}
if (!enableAccountDeletion) {
logger.warn(ctx, `Account deletion is not enabled`);
throw new Error('Account deletion is not enabled');
}
logger.info(ctx, `Deleting account...`);
// verify the OTP
const client = getSupabaseServerClient();
const otpApi = createOtpApi(client);
const otpResult = await otpApi.verifyToken({
token: otp,
userId: user.id,
purpose: 'delete-personal-account',
});
if (!otpResult.valid) {
throw new Error('Invalid OTP');
}
// validate the user ID matches the nonce's user ID
if (otpResult.user_id !== user.id) {
logger.error(
ctx,
`This token was meant to be used by a different user. Exiting.`,
);
throw new Error('Nonce mismatch');
}
// create a new instance of the personal accounts service
const service = createDeletePersonalAccountService();
// delete the user's account and cancel all subscriptions
await service.deletePersonalAccount({
adminClient: getSupabaseServerAdminClient(),
userId: user.id,
userEmail: user.email ?? null,
});
// sign out the user after deleting their account
await client.auth.signOut();
logger.info(ctx, `Account request successfully sent`);
// clear the cache for all pages
revalidatePath('/', 'layout');
// redirect to the home page
redirect('/');
},
{},
);

View File

@@ -0,0 +1,68 @@
import 'server-only';
import { SupabaseClient } from '@supabase/supabase-js';
import { getLogger } from '@kit/shared/logger';
import { Database } from '@kit/supabase/database';
export function createDeletePersonalAccountService() {
return new DeletePersonalAccountService();
}
/**
* @name DeletePersonalAccountService
* @description Service for managing accounts in the application
* @param Database - The Supabase database type to use
* @example
* const client = getSupabaseClient();
* const accountsService = new DeletePersonalAccountService();
*/
class DeletePersonalAccountService {
private namespace = 'accounts.delete';
/**
* @name deletePersonalAccount
* Delete personal account of a user.
* This will delete the user from the authentication provider and cancel all subscriptions.
*
* Permissions are not checked here, as they are checked in the server action.
* USE WITH CAUTION. THE USER MUST HAVE THE NECESSARY PERMISSIONS.
*/
async deletePersonalAccount(params: {
adminClient: SupabaseClient<Database>;
userId: string;
userEmail: string | null;
}) {
const logger = await getLogger();
const userId = params.userId;
const ctx = { userId, name: this.namespace };
logger.info(
ctx,
'User requested to delete their personal account. Processing...',
);
// execute the deletion of the user
try {
await params.adminClient.auth.admin.deleteUser(userId);
logger.info(ctx, 'User successfully deleted!');
return {
success: true,
};
} catch (error) {
logger.error(
{
...ctx,
error,
},
'Encountered an error deleting user',
);
throw new Error('Error deleting user');
}
}
}