mirror of
https://github.com/jcreek/SvelteKitSaasBoilerplate.git
synced 2026-07-13 02:53:50 +00:00
Merge remote-tracking branch 'origin/master' into 35-replace-navbar-with-a-better-one
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<div class="container mx-auto p-6">
|
||||
<h1 class="text-3xl font-bold text-primary mb-4">Cookie Policy</h1>
|
||||
<p class="text-gray-700 mb-6">Effective Date: <span class="font-semibold">[Insert Date]</span></p>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">1. Introduction</h2>
|
||||
<p class="text-gray-600">
|
||||
This Cookie Policy explains how [Your Company/Website Name] ("we", "us", or "our") uses
|
||||
cookies and similar technologies to collect and store information when you visit our website.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">2. What Are Cookies?</h2>
|
||||
<p class="text-gray-600">
|
||||
Cookies are small text files stored on your device (computer, tablet, smartphone) by your web
|
||||
browser when you visit a website. They help us remember your preferences and improve your
|
||||
experience on our website.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">3. Types of Cookies We Use</h2>
|
||||
<ul class="list-disc pl-6 text-gray-600">
|
||||
<li>
|
||||
<span class="font-semibold">Essential Cookies</span>: These cookies are necessary for the
|
||||
website to function properly and cannot be disabled in our systems. They include login
|
||||
authentication and session management cookies.
|
||||
</li>
|
||||
<li>
|
||||
<span class="font-semibold">Performance Cookies</span>: These cookies help us understand how
|
||||
visitors interact with our website by collecting information on usage patterns, such as
|
||||
pages visited and links clicked.
|
||||
</li>
|
||||
<li>
|
||||
<span class="font-semibold">Functional Cookies</span>: These cookies allow us to remember
|
||||
choices you make (such as your language preference) to provide a more personalised
|
||||
experience.
|
||||
</li>
|
||||
<li>
|
||||
<span class="font-semibold">Targeting/Advertising Cookies</span>: These cookies are used to
|
||||
deliver relevant advertisements and track the effectiveness of our marketing campaigns.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">4. How We Use Cookies</h2>
|
||||
<p class="text-gray-600">
|
||||
We use cookies to improve the functionality and performance of our website, analyse visitor
|
||||
usage, and provide personalised content and advertisements. This helps us improve our website
|
||||
and better understand our audience.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">5. Managing Your Cookie Preferences</h2>
|
||||
<p class="text-gray-600">
|
||||
You have the right to accept or reject cookies. You can manage your cookie preferences by
|
||||
adjusting your browser settings. However, disabling certain cookies may affect your experience
|
||||
on our website. For specific instructions on managing cookies, please consult your browser's
|
||||
help documentation.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">6. Changes to Our Cookie Policy</h2>
|
||||
<p class="text-gray-600">
|
||||
We may update this Cookie Policy from time to time. Significant changes will be posted on this
|
||||
page. We encourage you to review this policy periodically to stay informed about our use of
|
||||
cookies.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">Contact Us</h2>
|
||||
<p class="text-gray-600">
|
||||
For questions about our Cookie Policy, please contact us at <a
|
||||
href="mailto:your-email@example.com"
|
||||
class="text-primary font-semibold">[Your Contact Email]</a
|
||||
>
|
||||
or visit our <a href="/contact" class="text-primary font-semibold">Contact Page</a>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,125 @@
|
||||
<div class="container mx-auto p-6">
|
||||
<h1 class="text-3xl font-bold text-primary mb-4">Privacy Policy</h1>
|
||||
<p class="text-gray-700 mb-6">Effective Date: <span class="font-semibold">[Insert Date]</span></p>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">1. Introduction</h2>
|
||||
<p class="text-gray-600">
|
||||
This Privacy Policy explains how we at [Your Company/Website Name] collect, use, and protect
|
||||
your personal information when you visit our website. We are committed to safeguarding your
|
||||
data and complying with relevant data protection laws, including the General Data Protection
|
||||
Regulation (GDPR).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">2. Information We Collect</h2>
|
||||
<p class="text-gray-600">
|
||||
We collect personal and non-personal information to provide our services and enhance your
|
||||
experience.
|
||||
</p>
|
||||
<ul class="list-disc pl-6 text-gray-600">
|
||||
<li>
|
||||
<span class="font-semibold">Personal Information</span>: Details like your name, email
|
||||
address, and payment information collected when you make a purchase or create an account.
|
||||
</li>
|
||||
<li>
|
||||
<span class="font-semibold">Usage Data</span>: Information such as IP address, browser type,
|
||||
and browsing history for analytics and functionality.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">3. How We Use Your Information</h2>
|
||||
<ul class="list-disc pl-6 text-gray-600">
|
||||
<li>To process orders and manage customer support.</li>
|
||||
<li>To improve website functionality and provide a better user experience.</li>
|
||||
<li>To send marketing emails, if you opt in (you may unsubscribe at any time).</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">4. Sharing Information with Third Parties</h2>
|
||||
<p class="text-gray-600">
|
||||
We work with trusted third-party providers to offer secure services, such as payment
|
||||
processing and authentication.
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-xl font-semibold">Stripe</h3>
|
||||
<p class="text-gray-600">
|
||||
For payment processing, we use <a
|
||||
href="https://stripe.com"
|
||||
target="_blank"
|
||||
class="text-primary font-semibold">Stripe</a
|
||||
>. Please refer to their
|
||||
<a href="https://stripe.com/privacy" target="_blank" class="text-primary font-semibold"
|
||||
>Privacy Policy</a
|
||||
> for details.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-xl font-semibold">Supabase</h3>
|
||||
<p class="text-gray-600">
|
||||
For database and authentication, we use <a
|
||||
href="https://supabase.com"
|
||||
target="_blank"
|
||||
class="text-primary font-semibold">Supabase</a
|
||||
>. See their
|
||||
<a href="https://supabase.com/privacy" target="_blank" class="text-primary font-semibold"
|
||||
>Privacy Policy</a
|
||||
> for more information.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">5. Cookies and Tracking</h2>
|
||||
<p class="text-gray-600">
|
||||
We use cookies to provide essential site functionality, analytics, and improved user
|
||||
experience. By continuing to use our site, you consent to our use of cookies. You can manage
|
||||
your cookie preferences in your browser settings. For more details, please refer to our <a
|
||||
href="/cookies"
|
||||
class="text-primary font-semibold">Cookie Policy</a
|
||||
>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">6. Your Data Rights</h2>
|
||||
<p class="text-gray-600">
|
||||
Under GDPR, you have the right to access, correct, delete, and restrict the processing of your
|
||||
personal data. To make a request, please contact us at <a
|
||||
href="mailto:your-email@example.com"
|
||||
class="text-primary font-semibold">[Your Contact Email]</a
|
||||
>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">7. Data Security</h2>
|
||||
<p class="text-gray-600">
|
||||
We take reasonable steps to protect your information through encryption and secure access
|
||||
protocols. However, no online service can be 100% secure.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">8. Policy Updates</h2>
|
||||
<p class="text-gray-600">
|
||||
We may update this policy periodically. Significant changes will be posted on this page, so
|
||||
please review it regularly.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-6">
|
||||
<h2 class="text-2xl font-semibold mb-2">Contact Us</h2>
|
||||
<p class="text-gray-600">
|
||||
If you have questions about this Privacy Policy, please contact us at <a
|
||||
href="mailto:your-email@example.com"
|
||||
class="text-primary font-semibold">[Your Contact Email]</a
|
||||
>
|
||||
or visit our <a href="/contact" class="text-primary font-semibold">Contact Page</a>.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
@@ -69,8 +69,12 @@
|
||||
d="M22.672 15.226l-2.432.811.841 2.515c.33 1.019-.209 2.127-1.23 2.456-1.15.325-2.148-.321-2.463-1.226l-.84-2.518-5.013 1.677.84 2.517c.391 1.203-.434 2.542-1.831 2.542-.88 0-1.601-.564-1.86-1.314l-.842-2.516-2.431.809c-1.135.328-2.145-.317-2.463-1.229-.329-1.018.211-2.127 1.231-2.456l2.432-.809-1.621-4.823-2.432.808c-1.355.384-2.558-.59-2.558-1.839 0-.817.509-1.582 1.327-1.846l2.433-.809-.842-2.515c-.33-1.02.211-2.129 1.232-2.458 1.02-.329 2.13.209 2.461 1.229l.842 2.515 5.011-1.677-.839-2.517c-.403-1.238.484-2.553 1.843-2.553.819 0 1.585.509 1.85 1.326l.841 2.517 2.431-.81c1.02-.33 2.131.211 2.461 1.229.332 1.018-.21 2.126-1.23 2.456l-2.433.809 1.622 4.823 2.433-.809c1.242-.401 2.557.484 2.557 1.838 0 .819-.51 1.583-1.328 1.847m-8.992-6.428l-5.01 1.675 1.619 4.828 5.011-1.674-1.62-4.829z"
|
||||
></path>
|
||||
</svg>
|
||||
<p>Copyright © 2024 - All right reserved</p>
|
||||
<p>Copyright © 2024 - All rights reserved</p>
|
||||
</aside>
|
||||
<div class="grid-flow-col gap-4">
|
||||
<a href="/privacy">Privacy Policy</a>
|
||||
<a href="/cookies">Cookie Policy</a>
|
||||
</div>
|
||||
<nav class="grid-flow-col gap-4 md:place-self-center md:justify-self-end">
|
||||
<a href="/">
|
||||
<svg
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import logger from '$lib/utils/logger/logger';
|
||||
import { getUserSubscriptions, getUserTransactions } from '$lib/utils/supabase/admin';
|
||||
import { requestAccountDeletion } from '$lib/utils/supabase/admin';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } }) => {
|
||||
const { session } = await safeGetSession();
|
||||
@@ -14,10 +17,35 @@ export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession
|
||||
.eq('id', session.user.id)
|
||||
.single();
|
||||
|
||||
return { session, user };
|
||||
const subscriptions = await getUserSubscriptions(session.user.id);
|
||||
|
||||
const transactions = await getUserTransactions(session.user.id);
|
||||
|
||||
return { session, user, subscriptions, transactions };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
delete: async ({ url, locals: { safeGetSession } }) => {
|
||||
const { session } = await safeGetSession();
|
||||
const baseUrl = url.origin;
|
||||
if (session) {
|
||||
if (!session.user.email) {
|
||||
return fail(400, { message: 'Email is required for account deletion' });
|
||||
}
|
||||
try {
|
||||
await requestAccountDeletion(session.user.id, session.user.email, baseUrl);
|
||||
return {
|
||||
success: true,
|
||||
message: 'Account deletion request submitted successfully'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to request account deletion:', error);
|
||||
return fail(500, { message: 'Failed to process deletion request' });
|
||||
}
|
||||
} else {
|
||||
redirect(303, '/');
|
||||
}
|
||||
},
|
||||
update: async ({ request, locals: { supabase, safeGetSession } }) => {
|
||||
const formData = await request.formData();
|
||||
const name = formData.get('name') as string;
|
||||
@@ -27,27 +55,28 @@ export const actions: Actions = {
|
||||
if (!session) {
|
||||
return fail(401, { name });
|
||||
}
|
||||
const { error } = await supabase.from('users').update({
|
||||
name: name,
|
||||
}).eq('id', session?.user.id);
|
||||
const { error } = await supabase
|
||||
.from('users')
|
||||
.update({
|
||||
name: name
|
||||
})
|
||||
.eq('id', session?.user.id);
|
||||
|
||||
console.error(error);
|
||||
if (error) {
|
||||
logger.error('Failed to update user name', {
|
||||
userId: session?.user.id,
|
||||
error
|
||||
});
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return fail(500, {
|
||||
name,
|
||||
name
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
name
|
||||
};
|
||||
},
|
||||
signout: async ({ locals: { supabase, safeGetSession } }) => {
|
||||
const { session } = await safeGetSession();
|
||||
if (session) {
|
||||
await supabase.auth.signOut();
|
||||
redirect(303, '/');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+160
-25
@@ -5,7 +5,7 @@
|
||||
export let data;
|
||||
export let form;
|
||||
|
||||
let { session, supabase, user } = data;
|
||||
let { session, user, subscriptions, transactions } = data;
|
||||
$: ({ session, supabase, user } = data);
|
||||
|
||||
let profileForm: HTMLFormElement;
|
||||
@@ -26,34 +26,169 @@
|
||||
update();
|
||||
};
|
||||
};
|
||||
|
||||
const handleAccountDeletion: SubmitFunction = () => {
|
||||
loading = true;
|
||||
if (session.user && session.user.email) {
|
||||
alert('Check your email to confirm account deletion.');
|
||||
} else {
|
||||
alert('Please sign in to delete your account.');
|
||||
}
|
||||
|
||||
return async () => {
|
||||
loading = false;
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="form-widget">
|
||||
<form
|
||||
class="form-widget"
|
||||
method="post"
|
||||
action="?/update"
|
||||
use:enhance={handleSubmit}
|
||||
bind:this={profileForm}
|
||||
>
|
||||
<div>
|
||||
<label for="name">Name</label>
|
||||
<input id="name" name="name" type="text" value={form?.name ?? name} />
|
||||
<div
|
||||
class="account-page max-w-4xl mx-auto p-6 bg-base-100 rounded-lg shadow-md space-y-6 overflow-y-auto"
|
||||
>
|
||||
<!-- Flex container for two columns -->
|
||||
<div class="flex flex-col lg:flex-row gap-6">
|
||||
<!-- Left Column: Account Overview, Subscriptions, and Billing History -->
|
||||
<div class="flex-1 space-y-6">
|
||||
<!-- Account Overview -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-xl font-semibold">Account Overview</h2>
|
||||
<div class="form-control">
|
||||
<label for="email" class="label font-medium">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
class="input input-bordered"
|
||||
value={session.user.email}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">Signed in with magic link.</div>
|
||||
<form
|
||||
class="form-widget space-y-4"
|
||||
method="post"
|
||||
action="?/update"
|
||||
use:enhance={handleSubmit}
|
||||
bind:this={profileForm}
|
||||
>
|
||||
<div class="form-control">
|
||||
<label for="name" class="label font-medium">Name</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={form?.name ?? name}
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Enter your name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
class="btn btn-primary w-full"
|
||||
value={loading ? 'Loading...' : 'Update Name'}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Active Subscriptions -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-xl font-semibold">Subscriptions</h2>
|
||||
{#if subscriptions.length === 0}
|
||||
<p class="text-gray-600">You do not have any subscriptions.</p>
|
||||
{:else}
|
||||
<ul class="space-y-3">
|
||||
{#each subscriptions as subscription}
|
||||
<li class="p-4 border border-gray-200 rounded-md">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="font-semibold">{subscription.productName}</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
Price: {subscription.amount}
|
||||
{subscription.currency} / {subscription.interval}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
Expiry Date: {subscription.expiryDate}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
Status: {subscription.status}
|
||||
</p>
|
||||
</div>
|
||||
<!-- <button class="btn btn-secondary btn-sm">Manage</button> -->
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- Billing History -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-xl font-semibold">Billing History</h2>
|
||||
<ul class="space-y-3">
|
||||
{#if transactions}
|
||||
{#each transactions as transaction}
|
||||
<li class="flex justify-between p-4 border border-gray-200 rounded-md">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">Date: {transaction.created}</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
Amount: {transaction.amount}
|
||||
{transaction.currency}
|
||||
</p>
|
||||
</div>
|
||||
{#if transaction.receipt_url}
|
||||
<a href={transaction.receipt_url} class="btn btn-outline btn-sm" target="_blank">
|
||||
Download Receipt
|
||||
</a>
|
||||
{:else}
|
||||
<span class="text-gray-500">Receipt unavailable</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
class="button block primary"
|
||||
value={loading ? 'Loading...' : 'Update'}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Right Column: Notification Preferences and Feedback -->
|
||||
<div class="flex-none w-full lg:w-64 space-y-6">
|
||||
<!-- Notification Preferences -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-xl font-semibold">Notification Preferences</h2>
|
||||
<form method="post" action="?/update_notifications" class="space-y-2">
|
||||
<label class="flex items-center space-x-3">
|
||||
<input type="checkbox" class="checkbox checkbox-primary" />
|
||||
<!-- checked={user.notifications.productUpdates} -->
|
||||
<span>Product Updates</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-3">
|
||||
<input type="checkbox" class="checkbox checkbox-primary" />
|
||||
<!-- checked={user.notifications.billingReminders} -->
|
||||
<span>Billing Reminders</span>
|
||||
</label>
|
||||
<label class="flex items-center space-x-3">
|
||||
<input type="checkbox" class="checkbox checkbox-primary" />
|
||||
<!-- checked={user.notifications.promotions} -->
|
||||
<span>Promotional Emails</span>
|
||||
</label>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<form method="post" action="?/signout" use:enhance={handleSignOut}>
|
||||
<div>
|
||||
<button class="button block" disabled={loading}>Sign Out</button>
|
||||
<!-- Feedback Section -->
|
||||
<section class="space-y-4">
|
||||
<h2 class="text-xl font-semibold">Feedback</h2>
|
||||
<p class="text-gray-600">
|
||||
We'd love to hear your thoughts! Please feel free to provide any feedback.
|
||||
</p>
|
||||
<a href="/contact" class="btn btn-outline w-full">Provide Feedback</a>
|
||||
</section>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="?/delete" use:enhance={handleAccountDeletion}>
|
||||
<div>
|
||||
<button class="button block" disabled={loading}>Delete My Account</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type RequestHandler, redirect } from '@sveltejs/kit';
|
||||
import { stripe as stripeClient } from '$lib/utils/stripe';
|
||||
import { getStripeCustomerId } from '$lib/utils/supabase/admin';
|
||||
import type Stripe from 'stripe';
|
||||
import logger from '$lib/utils/logger/logger';
|
||||
|
||||
// Create a checkout session
|
||||
export const POST: RequestHandler = async ({ request, cookies, locals: { safeGetSession } }) => {
|
||||
@@ -34,7 +35,7 @@ export const POST: RequestHandler = async ({ request, cookies, locals: { safeGet
|
||||
try {
|
||||
stripeCustomerId = await getStripeCustomerId(userEmail!, userId);
|
||||
} catch (error) {
|
||||
console.error('Error fetching Stripe customer ID:', error);
|
||||
logger.error('Error fetching Stripe customer ID:', error);
|
||||
}
|
||||
|
||||
const stripeCheckoutSessionObject = {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { upsertCustomerToSupabase } from '$lib/utils/supabase/admin';
|
||||
|
||||
export const GET: RequestHandler = async ({ url, locals: { safeGetSession } }) => {
|
||||
const stripeCustomerId = url.searchParams.get('stripeCustomerId');
|
||||
const { session } = await safeGetSession();
|
||||
|
||||
if (!session || !stripeCustomerId) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
await upsertCustomerToSupabase(session?.user.id, stripeCustomerId);
|
||||
|
||||
return json({ message: 'Customer created' });
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { deleteAccount } from '$lib/utils/supabase/admin';
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
const token = url.searchParams.get('token');
|
||||
if (!token) {
|
||||
return new Response('Invalid token', { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteAccount(token);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return new Response('Failed to delete account', { status: 500 });
|
||||
}
|
||||
|
||||
return new Response('Account deleted successfully', { status: 200 });
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export const GET = async () => {
|
||||
return json(await getSomeData());
|
||||
};
|
||||
|
||||
async function getSomeData() {
|
||||
// Do your async fetching here
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { type RequestHandler, redirect } from '@sveltejs/kit';
|
||||
import { stripe as stripeClient } from '$lib/utils/stripe';
|
||||
import { getStripeCustomerId } from '$lib/utils/supabase/admin';
|
||||
import type Stripe from 'stripe';
|
||||
import logger from '$lib/utils/logger/logger';
|
||||
|
||||
// Create a subscription checkout session
|
||||
export const POST: RequestHandler = async ({ request, cookies, locals: { safeGetSession } }) => {
|
||||
@@ -29,7 +30,7 @@ export const POST: RequestHandler = async ({ request, cookies, locals: { safeGet
|
||||
try {
|
||||
stripeCustomerId = await getStripeCustomerId(userEmail!, userId);
|
||||
} catch (error) {
|
||||
console.error('Error fetching Stripe customer ID:', error);
|
||||
logger.error('Error fetching Stripe customer ID:', error);
|
||||
}
|
||||
|
||||
const stripeCheckoutSessionObject = {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
recordProductPurchase
|
||||
} from '$lib/utils/supabase/admin';
|
||||
import { stripe as stripeClient } from '$lib/utils/stripe';
|
||||
import logger from '$lib/utils/logger/logger';
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
const body = await request.text();
|
||||
@@ -26,7 +27,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
try {
|
||||
event = stripeClient.webhooks.constructEvent(body, signature, STRIPE_ENDPOINT_SECRET);
|
||||
} catch (err) {
|
||||
console.error(`⚠️ Webhook signature verification failed.`, err.message);
|
||||
logger.error(`⚠️ Webhook signature verification failed.`, err.message);
|
||||
return {
|
||||
status: 400,
|
||||
body: {}
|
||||
@@ -104,7 +105,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.log(`Unhandled event type ${event.type}`);
|
||||
logger.info(`Unhandled event type ${event.type}`);
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'An unknown error has occurred';
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import Stripe from 'stripe';
|
||||
import { getProductById } from '$lib/utils/supabase/admin';
|
||||
import { getProductById, upsertCustomerToSupabase } from '$lib/utils/supabase/admin';
|
||||
import logger from '$lib/utils/logger/logger';
|
||||
|
||||
export const load = async ({ fetch, locals: { safeGetSession } }) => {
|
||||
const { session } = await safeGetSession();
|
||||
|
||||
export const load = async ({ fetch }) => {
|
||||
const response = await fetch('/api/checkout/status');
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch checkout status');
|
||||
logger.error('Failed to fetch checkout status');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -24,13 +27,11 @@ export const load = async ({ fetch }) => {
|
||||
lineItem.productDescription = product.description ?? '';
|
||||
}
|
||||
|
||||
// Save the stripe customer id in the customers table
|
||||
const stripeCustomerId = checkoutSession.customer as string;
|
||||
const createCustomerResponse = await fetch(
|
||||
'/api/checkout/create-customer?stripeCustomerId=' + stripeCustomerId
|
||||
);
|
||||
if (!createCustomerResponse.ok) {
|
||||
console.error('Failed to create customer');
|
||||
if (session?.user.id) {
|
||||
const stripeCustomerId = checkoutSession.customer as string;
|
||||
await upsertCustomerToSupabase(session.user.id, stripeCustomerId);
|
||||
} else {
|
||||
logger.error('User ID is undefined');
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { Actions } from '@sveltejs/kit';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { sendEmail } from '$lib/utils/brevo/email';
|
||||
import { VITE_CONTACT_EMAIL } from '$env/static/private';
|
||||
import logger from '$lib/utils/logger/logger';
|
||||
import { isRateLimited } from '$lib/utils/rateLimiter';
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request }) => {
|
||||
const ip =
|
||||
request.headers.get('x-forwarded-for') ?? request.headers.get('remote-addr') ?? 'unknown';
|
||||
|
||||
if (isRateLimited(ip)) {
|
||||
return fail(429, { error: 'Too many requests. Please try again later.' });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const name = formData.get('name') as string;
|
||||
const email = formData.get('email') as string;
|
||||
const message = formData.get('message') as string;
|
||||
|
||||
const emailContent = `
|
||||
<p>You have a new contact form submission:</p>
|
||||
<p><strong>Name:</strong> ${name}</p>
|
||||
<p><strong>Email:</strong> ${email}</p>
|
||||
<p><strong>Message:</strong></p>
|
||||
<p>${message}</p>
|
||||
`;
|
||||
|
||||
try {
|
||||
await sendEmail('New Contact Form Submission', emailContent, VITE_CONTACT_EMAIL);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logger.error('Failed to send email:', error);
|
||||
return fail(400, { error });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { SubmitFunction } from '@sveltejs/kit';
|
||||
|
||||
let name = '';
|
||||
let email = '';
|
||||
let message = '';
|
||||
let submitted = false;
|
||||
let error = '';
|
||||
|
||||
const handleSubmit: SubmitFunction = () => {
|
||||
let loading = false;
|
||||
return async ({ result, update }) => {
|
||||
loading = true;
|
||||
if (result.data && result.data.success) {
|
||||
submitted = true;
|
||||
} else {
|
||||
error = result.error?.message || 'Form submission failed';
|
||||
}
|
||||
loading = false;
|
||||
await update();
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if submitted}
|
||||
<div class="alert alert-success shadow-lg">
|
||||
<div>
|
||||
<span>Thank you for your message, {name}!</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<form
|
||||
method="post"
|
||||
action="?"
|
||||
use:enhance={handleSubmit}
|
||||
class="max-w-lg mx-auto p-4 bg-base-200 rounded-lg shadow-md"
|
||||
aria-labelledby="contact-form-title"
|
||||
>
|
||||
<h2 id="contact-form-title" class="text-xl font-bold mb-4">Contact Us</h2>
|
||||
{#if error}
|
||||
<div class="alert alert-error shadow-lg mb-4">
|
||||
<div>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="form-control mb-4">
|
||||
<label for="name" class="label">
|
||||
<span class="label-text">Name *</span>
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
bind:value={name}
|
||||
class="input input-bordered"
|
||||
aria-required="true"
|
||||
minlength="2"
|
||||
maxlength="100"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label for="email" class="label">
|
||||
<span class="label-text">Email *</span>
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
bind:value={email}
|
||||
class="input input-bordered"
|
||||
aria-required="true"
|
||||
minlength="5"
|
||||
maxlength="100"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label for="message" class="label">
|
||||
<span class="label-text">Message *</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
bind:value={message}
|
||||
class="textarea textarea-bordered"
|
||||
aria-required="true"
|
||||
minlength="10"
|
||||
maxlength="1000"
|
||||
required
|
||||
></textarea>
|
||||
<small class="text-sm mt-1">Maximum 1000 characters</small>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<button type="submit" class="btn btn-primary">Send</button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
@@ -1,19 +1,37 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { getActiveProductsWithPrices } from '$lib/utils/supabase/admin';
|
||||
import logger from '$lib/utils/logger/logger';
|
||||
|
||||
export const GET = async ({ url }) => {
|
||||
const limitParam: string | null = url.searchParams.get('limit');
|
||||
const offsetParam: string | null = url.searchParams.get('offset');
|
||||
try {
|
||||
const limitParam = url.searchParams.get('limit');
|
||||
const offsetParam = url.searchParams.get('offset');
|
||||
|
||||
const limit = limitParam ? parseInt(limitParam, 10) : 10;
|
||||
const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
|
||||
const limit = limitParam ? Number.parseInt(limitParam, 10) : 10;
|
||||
const offset = offsetParam ? Number.parseInt(offsetParam, 10) : 0;
|
||||
|
||||
const { products, count } = await getActiveProductsWithPrices(limit, offset);
|
||||
|
||||
return json({ products, count });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return json({ error: error.message }, { status: 500 });
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
logger.error('Failed to fetch products', {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
params: {
|
||||
limit: limitParam,
|
||||
offset: offsetParam
|
||||
}
|
||||
});
|
||||
return json({ error: error.message }, { status: 500 });
|
||||
} else {
|
||||
logger.error('Failed to fetch products', {
|
||||
error,
|
||||
params: {
|
||||
limit: limitParam,
|
||||
offset: offsetParam
|
||||
}
|
||||
});
|
||||
return json({ error }, { status: 500 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user