Initial project scaffolding

This commit is contained in:
2026-03-25 22:34:03 -04:00
commit a46c97c88a
92 changed files with 9671 additions and 0 deletions

115
app/stores/auth.ts Normal file
View File

@@ -0,0 +1,115 @@
import { defineStore } from 'pinia'
import type { Database, MemberRole } from '~/types/supabase'
export type Member = Database['public']['Tables']['members']['Row']
export const useAuthStore = defineStore('auth', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const supabase = useSupabaseClient() as any
const user = useSupabaseUser()
const member = ref<Member | null>(null)
const userNames = ref<Record<string, string>>({})
const role = computed<MemberRole | null>(() => member.value?.role ?? null)
function hasRequiredRole(requiredRoles: MemberRole[]): boolean {
if (!role.value) return false
return requiredRoles.includes(role.value)
}
const isAdmin = computed(() => hasRequiredRole(['admin']))
const isBoatswain = computed(() => hasRequiredRole(['admin', 'boatswain']))
const displayName = computed(() => {
if (!member.value) return user.value?.email ?? ''
const { first_name, last_name } = member.value
if (first_name || last_name) return `${first_name} ${last_name}`.trim()
return user.value?.email ?? ''
})
async function fetchMember() {
if (!user.value) { member.value = null; return }
const { data } = await supabase
.from('members')
.select('*')
.eq('user_id', user.value.id)
.single()
member.value = data
}
// Returns cached name or triggers async fetch; updates reactively when resolved.
function getUserNameById(id: string | null | undefined): string {
if (!id) return 'No User'
if (!userNames.value[id]) {
userNames.value[id] = 'Loading...'
supabase
.from('members')
.select('first_name, last_name')
.eq('user_id', id)
.single()
.then(({ data }: { data: Pick<Member, 'first_name' | 'last_name'> | null }) => {
if (data) {
userNames.value[id] = `${data.first_name} ${data.last_name}`.trim() || 'Unknown'
} else {
userNames.value[id] = 'Unknown'
}
})
}
return userNames.value[id]!
}
async function signOut() {
await supabase.auth.signOut()
member.value = null
await navigateTo('/login')
}
async function sendMagicLink(email: string) {
const { error } = await supabase.auth.signInWithOtp({
email,
options: { emailRedirectTo: window.location.origin + '/auth/callback' },
})
if (error) throw error
}
async function sendOtp(email: string) {
const { error } = await supabase.auth.signInWithOtp({ email })
if (error) throw error
}
async function verifyOtp(email: string, token: string) {
const { error } = await supabase.auth.verifyOtp({ email, token, type: 'email' })
if (error) throw error
await fetchMember()
}
async function updateName(firstName: string, lastName: string) {
if (!user.value) return
const { error } = await supabase
.from('members')
.update({ first_name: firstName, last_name: lastName })
.eq('user_id', user.value.id)
if (error) throw error
if (member.value) {
member.value.first_name = firstName
member.value.last_name = lastName
}
}
return {
user,
member,
role,
isAdmin,
isBoatswain,
displayName,
hasRequiredRole,
getUserNameById,
fetchMember,
sendMagicLink,
sendOtp,
verifyOtp,
updateName,
signOut,
}
})