Skip to content

Commit

Permalink
♻️ Refactor user authentication and state management features (#795)
Browse files Browse the repository at this point in the history
* ✨ Seed the storage.buckets table with default buckets in seed.sql
* ➕ Add `snakecase-keys`
* 🔧 Change `let` to `const` for `isSignUpPage` variable in AdminHeaderMessage.svelte
* 🔧 [backend] Update --schema option for `pnpm pull` command
* ♻️ import paths to use `$lib/supabase` instead of `$lib/supabaseClient`
* ♻️ Rename `[comment|user]Utils.ts` to `[comment|user]Requests.ts`
* ♻️ Refactor user authentication and profile management features
  * Update getUser function to include 'id' in the selected fields and use camelcaseKeys
  * Remove user input management and authentication methods from userStore.svelte.ts
  * Add userInputs state management in UserInputs.svelte
  • Loading branch information
usagizmo committed Jun 17, 2024
1 parent de4152b commit f7c4ab9
Show file tree
Hide file tree
Showing 19 changed files with 221 additions and 162 deletions.
2 changes: 1 addition & 1 deletion apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"pull": "supabase db pull --local --schema auth --schema storage",
"pull": "supabase db pull --local --schema auth,storage",
"start": "supabase start",
"stop": "supabase stop"
},
Expand Down
5 changes: 5 additions & 0 deletions apps/backend/supabase/seed.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Seed the storage.buckets table with the default buckets
insert into storage.buckets
(id, name, public, avif_autodetection, file_size_limit)
values
('comments', 'comments', true, false, 5242880);
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@supabase/supabase-js": "^2.43.4",
"camelcase-keys": "^9.1.3",
"cdate": "^0.0.7",
"snakecase-keys": "^8.0.1",
"tailwind-variants": "^0.2.1",
"type-fest": "^4.20.0"
},
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/features/comment/Comment.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import { userStore } from '$lib/features/user/userStore.svelte';
import { buttonVariants } from '$lib/variants/buttonVariants';
import { deleteCommentFile, getCommentFileUrl } from './commentRequests';
import type { Comment } from './commentStore.svelte';
import { commentStore } from './commentStore.svelte';
import { deleteCommentFile, getCommentFileUrl } from './commentUtils';
interface Card {
id: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { supabase } from '$lib/supabaseClient';
import { supabase } from '$lib/supabase';

/**
* Uploads a file to the comments storage bucket
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/lib/features/comment/commentStore.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { PostgrestError } from '@supabase/supabase-js';

import { userStore } from '$lib/features/user/userStore.svelte';
import { supabase } from '$lib/supabaseClient';
import { supabase } from '$lib/supabase';

import { deleteCommentFile, uploadCommentFile } from './commentUtils';
import { deleteCommentFile, uploadCommentFile } from './commentRequests';

export interface Comment {
id: number;
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/lib/features/user/OnAuthStateChange.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
import type { AuthSession } from '@supabase/supabase-js';
import { onMount } from 'svelte';
import { getUser } from '$lib/features/user/userRequests';
import { userStore } from '$lib/features/user/userStore.svelte';
import { supabase } from '$lib/supabase';
let session = $state<AuthSession | null>(null);
$effect(() => {
if (!session) {
userStore.user = null;
return;
}
getUser(session.user.id).then(({ user }) => {
userStore.user = user;
});
});
onMount(() => {
supabase.auth.getSession().then(({ data }) => {
session = data.session;
});
supabase.auth.onAuthStateChange((_event, _session) => {
session = _session;
});
});
</script>
72 changes: 72 additions & 0 deletions apps/web/src/lib/features/user/userRequests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { AuthError, PostgrestError } from '@supabase/supabase-js';
import camelcaseKeys from 'camelcase-keys';

import { supabase } from '$lib/supabase';

import type { User } from './userStore.svelte';

/**
* Sign up a user
* @param inputs User inputs
* @param inputs.email User email
* @param inputs.password User password
* @param inputs.displayName User display name
* @returns Error
*/
export async function signUp(inputs: {
email: string;
password: string;
displayName: string;
}): Promise<{ error: AuthError | null }> {
const { email, password, displayName } = inputs;
const { error } = await supabase.auth.signUp({
email,
password,
options: {
data: { display_name: displayName },
},
});
return { error };
}

/**
* Sign in a user
* @param inputs User inputs
* @param inputs.email User email
* @param inputs.password User password
* @returns Error
*/
export async function signIn(inputs: {
email: string;
password: string;
}): Promise<{ error: AuthError | null }> {
const { error } = await supabase.auth.signInWithPassword(inputs);
return { error };
}

/**
* Sign out a user
* @returns Error
*/
export async function signOut(): Promise<{ error: AuthError | null }> {
const { error } = await supabase.auth.signOut();
return { error };
}

/**
* Get a user (profile) by id
* @param id user id
* @returns user (profile) and error
*/
export async function getUser(
id: string,
): Promise<{ user: User | null; error: PostgrestError | null }> {
const { data: profiles, error } = await supabase
.from('profiles')
.select('id, email, display_name, bio, created_at')
.eq('id', id);

const user = profiles?.length ? (camelcaseKeys(profiles[0]) satisfies User) : null;

return { user, error };
}
80 changes: 12 additions & 68 deletions apps/web/src/lib/features/user/userStore.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
import type { PostgrestError } from '@supabase/supabase-js';
import camelcaseKeys from 'camelcase-keys';
import type { CamelCasedProperties } from 'type-fest';
import snakecaseKeys from 'snakecase-keys';

import type { Tables } from '$lib/$generated/supabase-types';
import { supabase } from '$lib/supabaseClient';
import { supabase } from '$lib/supabase';

type DBProfiles = Tables<'profiles'>;

export type User = CamelCasedProperties<DBProfiles>;

export interface UserInputs {
export interface User {
id: string;
email: string;
password: string;
displayName?: string;
displayName: string;
bio: string;
createdAt: string;
}

const baseUserInputs = {
displayName: 'Guest',
email: 'email@add.com',
password: 'password0',
} satisfies UserInputs;

class UserStore {
#user = $state<User | null>(null);
#userInputs = $state<UserInputs>(baseUserInputs);

/**
* Get the user
Expand All @@ -42,64 +31,19 @@ class UserStore {

async updateUser(
id: string,
props: Partial<Omit<DBProfiles, 'id'>>,
props: Partial<Pick<User, 'bio'>>,
): Promise<{ error: PostgrestError | Error | null }> {
if (!this.#user) return { error: new Error('You must be logged in to update your profile') };

const { error } = await supabase.from('profiles').update(props).eq('id', id);
// NOTE: $state.snapshot is used to get the current value of the reactive variable
const plainSnakeProps = snakecaseKeys($state.snapshot(props));
const { error } = await supabase.from('profiles').update(plainSnakeProps).eq('id', id);
if (error) return { error };

this.#user = { ...this.#user, ...camelcaseKeys(props) };
this.#user = { ...this.#user, ...props };

return { error: null };
}

/**
* Get the user inputs
* @returns The user inputs
*/
get userInputs(): UserInputs {
return this.#userInputs;
}

/**
* Update the user inputs
* @param userInputs - The user inputs
*/
updateUserInputs(userInputs: Partial<UserInputs>): void {
this.#userInputs = { ...this.#userInputs, ...userInputs };
}

/**
* Sign up and log in
*/
async signUp(): Promise<void> {
const { email, password, displayName } = this.#userInputs;
const { error } = await supabase.auth.signUp({
email,
password,
options: {
data: { display_name: displayName },
},
});
error && alert(error.message);
}

/**
* Log in a user
*/
async logIn(): Promise<void> {
const { error } = await supabase.auth.signInWithPassword(this.#userInputs);
error && alert(error.message);
}

/**
* Log out a user
*/
async logOut(): Promise<void> {
const { error } = await supabase.auth.signOut();
error && alert(error.message);
}
}

export const userStore = new UserStore();
30 changes: 0 additions & 30 deletions apps/web/src/lib/features/user/userUtils.ts

This file was deleted.

File renamed without changes.
31 changes: 3 additions & 28 deletions apps/web/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
<script lang="ts">
import '../app.css';
import type { AuthSession } from '@supabase/supabase-js';
import type { Snippet } from 'svelte';
import { onMount } from 'svelte';
import { userStore } from '$lib/features/user/userStore.svelte';
import { getUser } from '$lib/features/user/userUtils';
import { supabase } from '$lib/supabaseClient';
import OnAuthStateChange from '$lib/features/user/OnAuthStateChange.svelte';
import Footer from './Footer.svelte';
import HeaderNavigation from './HeaderNavigation.svelte';
Expand All @@ -17,31 +13,10 @@
}: {
children: Snippet;
} = $props();
let session = $state<AuthSession | null>(null);
$effect(() => {
if (!session) {
userStore.user = null;
return;
}
getUser(session.user.id).then(({ user }) => {
userStore.user = user;
});
});
onMount(() => {
supabase.auth.getSession().then(({ data }) => {
session = data.session;
});
supabase.auth.onAuthStateChange((_event, _session) => {
session = _session;
});
});
</script>

<OnAuthStateChange />

<div class="h-screen bg-gray-100 text-zinc-900">
<div class="flex h-full flex-col">
<HeaderNavigation />
Expand Down
21 changes: 14 additions & 7 deletions apps/web/src/routes/admin/(isNotLoggedIn)/UserInputs.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
<script lang="ts" context="module">
export let userInputs = $state({
displayName: 'Guest',
email: 'email@add.com',
password: 'password0',
});
</script>

<script lang="ts">
import { slide } from 'svelte/transition';
import { page } from '$app/stores';
import Input from '$lib/components/Input.svelte';
import { defaultDE } from '$lib/easing';
import { userStore } from '$lib/features/user/userStore.svelte';
import { ROUTE } from '$lib/routes';
const isSignUpPage = $derived($page.url.pathname === ROUTE.ADMIN_SIGNUP);
Expand All @@ -16,24 +23,24 @@
<Input
label="Display Name"
type="text"
value={userStore.userInputs.displayName}
oninput={(event) => userStore.updateUserInputs({ displayName: (event.target as HTMLInputElement).value })}
value={userInputs.displayName}
oninput={(event) => userInputs.displayName = (event.target as HTMLInputElement).value}
error={{ required: 'Display Name is required.' }}
/>
</div>
{/if}
<Input
label="Email"
type="email"
value={userStore.userInputs.email}
oninput={(event) => userStore.updateUserInputs({ email: (event.target as HTMLInputElement).value })}
value={userInputs.email}
oninput={(event) => userInputs.email = (event.target as HTMLInputElement).value}
error={{ required: 'E-mail is required.' }}
/>
<Input
label="Password"
type="password"
value={userStore.userInputs.password}
oninput={(event) => userStore.updateUserInputs({ password: (event.target as HTMLInputElement).value })}
value={userInputs.password}
oninput={(event) => userInputs.password = (event.target as HTMLInputElement).value}
error={{ required: 'Password is required.' }}
/>
</div>
Loading

0 comments on commit f7c4ab9

Please sign in to comment.