Added supabase auth to project
This design is minimalist for now, it might be extended (at least with a basic profil page or command list)
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -22,3 +22,7 @@ logs
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
|
# Supabase
|
||||||
|
.branches
|
||||||
|
.temp
|
||||||
|
|||||||
@@ -2,55 +2,55 @@
|
|||||||
import { useCalendar } from '@/stores/CalendarStore'
|
import { useCalendar } from '@/stores/CalendarStore'
|
||||||
|
|
||||||
import { PhList, PhHouse, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
import { PhList, PhHouse, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
|
||||||
import Button from '../ui/button/Button.vue'
|
|
||||||
|
|
||||||
const { revealAdvancedSearch } = useCalendar()
|
const { revealAdvancedSearch } = useCalendar()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="w-16 py-6 border-r-[1px] border-l-slate-500 grid justify-center">
|
<nav class="w-16 py-6 border-r-[1px] border-l-slate-500 grid grid-rows-[1fr_auto] justify-center">
|
||||||
<menu class="flex flex-col gap-4">
|
<menu class="flex flex-col gap-4">
|
||||||
<li class="mb-12">
|
<li class="mb-12">
|
||||||
<Button variant="ghost" size="icon" class="rounded-full" @click="console.log">
|
<UiButton variant="ghost" size="icon" class="rounded-full" @click="console.log">
|
||||||
<PhList size="27" />
|
<PhList size="27" />
|
||||||
</Button>
|
</UiButton>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<TooltipProvider :delay-duration="100">
|
<UiTooltipProvider :delay-duration="100">
|
||||||
<Tooltip>
|
<UiTooltip>
|
||||||
<TooltipTrigger as-child>
|
<UiTooltipTrigger as-child>
|
||||||
<Button variant="ghost" size="icon" class="rounded-full" as-child>
|
<UiButton variant="ghost" size="icon" class="rounded-full" as-child>
|
||||||
<RouterLink to="/">
|
<RouterLink to="/">
|
||||||
<PhHouse size="24" weight="fill" />
|
<PhHouse size="24" weight="fill" />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</Button>
|
</UiButton>
|
||||||
</TooltipTrigger>
|
</UiTooltipTrigger>
|
||||||
<TooltipContent :side="'right'">
|
<UiTooltipContent :side="'right'">
|
||||||
<p>Retourner aux outils</p>
|
<p>Retourner aux outils</p>
|
||||||
</TooltipContent>
|
</UiTooltipContent>
|
||||||
</Tooltip>
|
</UiTooltip>
|
||||||
</TooltipProvider>
|
</UiTooltipProvider>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<TooltipProvider :delay-duration="100">
|
<UiTooltipProvider :delay-duration="100">
|
||||||
<Tooltip>
|
<UiTooltip>
|
||||||
<TooltipTrigger as-child>
|
<UiTooltipTrigger as-child>
|
||||||
<Button
|
<UiButton
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="rounded-full"
|
class="rounded-full"
|
||||||
@click="revealAdvancedSearch()"
|
@click="revealAdvancedSearch()"
|
||||||
>
|
>
|
||||||
<PhMagnifyingGlass size="24" weight="fill" />
|
<PhMagnifyingGlass size="24" weight="fill" />
|
||||||
</Button>
|
</UiButton>
|
||||||
</TooltipTrigger>
|
</UiTooltipTrigger>
|
||||||
<TooltipContent :side="'right'">
|
<UiTooltipContent :side="'right'">
|
||||||
<p>Recherche avancée</p>
|
<p>Recherche avancée</p>
|
||||||
</TooltipContent>
|
</UiTooltipContent>
|
||||||
</Tooltip>
|
</UiTooltip>
|
||||||
</TooltipProvider>
|
</UiTooltipProvider>
|
||||||
</li>
|
</li>
|
||||||
</menu>
|
</menu>
|
||||||
|
|
||||||
|
<UserCTA />
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const moveCalendarRight = useThrottleFn(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container mt-[10vh] mb-auto" @wheel="handleWheel">
|
<div class="container mt-[10vh] mb-auto" @wheel="handleWheel">
|
||||||
<div ref="test" class="grid grid-cols-5 gap-x-8 gap-y-16">
|
<div class="grid grid-cols-5 gap-x-8 gap-y-16">
|
||||||
<MonthTile
|
<MonthTile
|
||||||
v-for="month in staticConfig.monthsPerYear"
|
v-for="month in staticConfig.monthsPerYear"
|
||||||
:key="month"
|
:key="month"
|
||||||
|
|||||||
77
components/global/user/CTA.vue
Normal file
77
components/global/user/CTA.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import { PhUserCircle } from '@phosphor-icons/vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const { auth } = useSupabaseClient()
|
||||||
|
const user = useSupabaseUser()
|
||||||
|
const userMeta = computed(() => user.value?.user_metadata)
|
||||||
|
const hasSession = computed(() => Boolean(user.value))
|
||||||
|
|
||||||
|
async function handleGoogleLogin() {
|
||||||
|
try {
|
||||||
|
auth.signInWithOAuth({
|
||||||
|
provider: 'google',
|
||||||
|
options: {
|
||||||
|
queryParams: {
|
||||||
|
access_type: 'offline',
|
||||||
|
prompt: 'consent'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogout() {
|
||||||
|
try {
|
||||||
|
const { error } = await auth.signOut()
|
||||||
|
|
||||||
|
if (error) throw error
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function gotoProfilePage() {
|
||||||
|
router.push({ path: '/profil' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ClientOnly>
|
||||||
|
<UiPopover>
|
||||||
|
<UiPopoverTrigger :as-child="hasSession">
|
||||||
|
<UiAvatar v-if="hasSession" class="cursor-pointer">
|
||||||
|
<UiAvatarImage
|
||||||
|
:src="userMeta?.avatar_url"
|
||||||
|
:alt="userMeta?.full_name"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
<UiAvatarFallback>Me</UiAvatarFallback>
|
||||||
|
</UiAvatar>
|
||||||
|
<UiButton v-else variant="outline" size="icon">
|
||||||
|
<PhUserCircle size="18" />
|
||||||
|
</UiButton>
|
||||||
|
</UiPopoverTrigger>
|
||||||
|
<UiPopoverContent class="w-fit p-0" :align="'start'" :side="'top'" :collision-padding="20">
|
||||||
|
<div id="user-popover">
|
||||||
|
<UiCommand>
|
||||||
|
<UiCommandList v-if="user">
|
||||||
|
<UiCommandGroup :heading="`Connecté en tant que ${user?.email}`">
|
||||||
|
<UiCommandItem value="profile" @select="gotoProfilePage"> Profil </UiCommandItem>
|
||||||
|
<UiCommandItem value="logout" @select="handleLogout"> Déconnexion </UiCommandItem>
|
||||||
|
</UiCommandGroup>
|
||||||
|
</UiCommandList>
|
||||||
|
<UiCommandList v-else>
|
||||||
|
<UiCommandItem value="logout" @select="handleGoogleLogin"> Connexion </UiCommandItem>
|
||||||
|
</UiCommandList>
|
||||||
|
</UiCommand>
|
||||||
|
</div>
|
||||||
|
</UiPopoverContent>
|
||||||
|
</UiPopover>
|
||||||
|
</ClientOnly>
|
||||||
|
</template>
|
||||||
21
components/ui/avatar/Avatar.vue
Normal file
21
components/ui/avatar/Avatar.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { AvatarRoot } from 'radix-vue'
|
||||||
|
import { type AvatarVariants, avatarVariant } from '.'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
size?: AvatarVariants['size']
|
||||||
|
shape?: AvatarVariants['shape']
|
||||||
|
}>(), {
|
||||||
|
size: 'sm',
|
||||||
|
shape: 'circle',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)">
|
||||||
|
<slot />
|
||||||
|
</AvatarRoot>
|
||||||
|
</template>
|
||||||
11
components/ui/avatar/AvatarFallback.vue
Normal file
11
components/ui/avatar/AvatarFallback.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { AvatarFallback, type AvatarFallbackProps } from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<AvatarFallbackProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AvatarFallback v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</AvatarFallback>
|
||||||
|
</template>
|
||||||
9
components/ui/avatar/AvatarImage.vue
Normal file
9
components/ui/avatar/AvatarImage.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { AvatarImage, type AvatarImageProps } from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<AvatarImageProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AvatarImage v-bind="props" class="h-full w-full object-cover" />
|
||||||
|
</template>
|
||||||
24
components/ui/avatar/index.ts
Normal file
24
components/ui/avatar/index.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { type VariantProps, cva } from 'class-variance-authority'
|
||||||
|
|
||||||
|
export { default as Avatar } from './Avatar.vue'
|
||||||
|
export { default as AvatarImage } from './AvatarImage.vue'
|
||||||
|
export { default as AvatarFallback } from './AvatarFallback.vue'
|
||||||
|
|
||||||
|
export const avatarVariant = cva(
|
||||||
|
'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
sm: 'h-10 w-10 text-xs',
|
||||||
|
base: 'h-16 w-16 text-2xl',
|
||||||
|
lg: 'h-32 w-32 text-5xl',
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
circle: 'rounded-full',
|
||||||
|
square: 'rounded-md',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export type AvatarVariants = VariantProps<typeof avatarVariant>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export { ComboboxPortal } from 'radix-vue'
|
||||||
|
|
||||||
export { default as Command } from './Command.vue'
|
export { default as Command } from './Command.vue'
|
||||||
export { default as CommandDialog } from './CommandDialog.vue'
|
export { default as CommandDialog } from './CommandDialog.vue'
|
||||||
export { default as CommandEmpty } from './CommandEmpty.vue'
|
export { default as CommandEmpty } from './CommandEmpty.vue'
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ export default defineNuxtConfig({
|
|||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
|
|
||||||
modules: [
|
modules: [
|
||||||
'@nuxtjs/tailwindcss',
|
'@nuxtjs/supabase',
|
||||||
'shadcn-nuxt',
|
|
||||||
'@nuxtjs/color-mode',
|
|
||||||
'@pinia/nuxt',
|
'@pinia/nuxt',
|
||||||
|
'@vueuse/nuxt',
|
||||||
|
'@nuxtjs/tailwindcss',
|
||||||
|
'@nuxtjs/color-mode',
|
||||||
'@nuxt/eslint',
|
'@nuxt/eslint',
|
||||||
'@vueuse/nuxt'
|
'shadcn-nuxt'
|
||||||
],
|
],
|
||||||
css: ['~/assets/main.css'],
|
css: ['~/assets/main.css'],
|
||||||
|
|
||||||
@@ -27,5 +28,8 @@ export default defineNuxtConfig({
|
|||||||
preference: 'dark',
|
preference: 'dark',
|
||||||
fallback: 'dark',
|
fallback: 'dark',
|
||||||
},
|
},
|
||||||
|
supabase: {
|
||||||
|
redirect: false
|
||||||
|
},
|
||||||
eslint: {}
|
eslint: {}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,10 +14,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/eslint": "^0.3.12",
|
"@nuxt/eslint": "^0.3.12",
|
||||||
"@vueuse/nuxt": "^10.9.0",
|
|
||||||
"@phosphor-icons/vue": "^2.2.1",
|
"@phosphor-icons/vue": "^2.2.1",
|
||||||
"@pinia/nuxt": "^0.5.1",
|
"@pinia/nuxt": "^0.5.1",
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^10.9.0",
|
||||||
|
"@vueuse/nuxt": "^10.9.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-vue-next": "^0.364.0",
|
"lucide-vue-next": "^0.364.0",
|
||||||
@@ -32,6 +32,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxtjs/color-mode": "^3.4.1",
|
"@nuxtjs/color-mode": "^3.4.1",
|
||||||
|
"@nuxtjs/supabase": "^1.2.2",
|
||||||
"@nuxtjs/tailwindcss": "^6.12.0",
|
"@nuxtjs/tailwindcss": "^6.12.0",
|
||||||
"@stylistic/eslint-plugin-js": "^2.1.0",
|
"@stylistic/eslint-plugin-js": "^2.1.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.8.0",
|
"@typescript-eslint/eslint-plugin": "^7.8.0",
|
||||||
|
|||||||
94
pnpm-lock.yaml
generated
94
pnpm-lock.yaml
generated
@@ -60,6 +60,9 @@ importers:
|
|||||||
'@nuxtjs/color-mode':
|
'@nuxtjs/color-mode':
|
||||||
specifier: ^3.4.1
|
specifier: ^3.4.1
|
||||||
version: 3.4.1(rollup@4.17.2)
|
version: 3.4.1(rollup@4.17.2)
|
||||||
|
'@nuxtjs/supabase':
|
||||||
|
specifier: ^1.2.2
|
||||||
|
version: 1.2.2(rollup@4.17.2)
|
||||||
'@nuxtjs/tailwindcss':
|
'@nuxtjs/tailwindcss':
|
||||||
specifier: ^6.12.0
|
specifier: ^6.12.0
|
||||||
version: 6.12.0(rollup@4.17.2)
|
version: 6.12.0(rollup@4.17.2)
|
||||||
@@ -705,6 +708,9 @@ packages:
|
|||||||
'@nuxtjs/color-mode@3.4.1':
|
'@nuxtjs/color-mode@3.4.1':
|
||||||
resolution: {integrity: sha512-vZgJqDstxInGw3RGSWbLoCLXtU1mvh1LLeuEA/X3a++DYA4ifwSbNoiSiOyb9qZHFEwz1Xr99H71sXV4IhOaEg==}
|
resolution: {integrity: sha512-vZgJqDstxInGw3RGSWbLoCLXtU1mvh1LLeuEA/X3a++DYA4ifwSbNoiSiOyb9qZHFEwz1Xr99H71sXV4IhOaEg==}
|
||||||
|
|
||||||
|
'@nuxtjs/supabase@1.2.2':
|
||||||
|
resolution: {integrity: sha512-DNEuB7YXaOhV118bNway6SUpQLGq0zsK1kzddyETrwx3dHz+7etEw+QKU7iVs/GZog9Q7xkUQtGhcEQYaTZ71w==}
|
||||||
|
|
||||||
'@nuxtjs/tailwindcss@6.12.0':
|
'@nuxtjs/tailwindcss@6.12.0':
|
||||||
resolution: {integrity: sha512-vXvEq8z177TQcx0tc10mw3O6T9WeN0iTL8hIKGDfidmr+HKReexJU01aPgHefFrCu4LJB70egYFYnywzB9lMyQ==}
|
resolution: {integrity: sha512-vXvEq8z177TQcx0tc10mw3O6T9WeN0iTL8hIKGDfidmr+HKReexJU01aPgHefFrCu4LJB70egYFYnywzB9lMyQ==}
|
||||||
|
|
||||||
@@ -1112,6 +1118,28 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: '>=8.40.0'
|
eslint: '>=8.40.0'
|
||||||
|
|
||||||
|
'@supabase/auth-js@2.64.1':
|
||||||
|
resolution: {integrity: sha512-tA2PXLoWEzhD0N1Vysree+HftfeWBbFV0E+taND5rj/pZTjkwKq/9GlrnXkbs5pnw+tsnABDRo2WLZmymihGdA==}
|
||||||
|
|
||||||
|
'@supabase/functions-js@2.3.1':
|
||||||
|
resolution: {integrity: sha512-QyzNle/rVzlOi4BbVqxLSH828VdGY1RElqGFAj+XeVypj6+PVtMlD21G8SDnsPQDtlqqTtoGRgdMlQZih5hTuw==}
|
||||||
|
|
||||||
|
'@supabase/node-fetch@2.6.15':
|
||||||
|
resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==}
|
||||||
|
engines: {node: 4.x || >=6.0.0}
|
||||||
|
|
||||||
|
'@supabase/postgrest-js@1.15.2':
|
||||||
|
resolution: {integrity: sha512-9/7pUmXExvGuEK1yZhVYXPZnLEkDTwxgMQHXLrN5BwPZZm4iUCL1YEyep/Z2lIZah8d8M433mVAUEGsihUj5KQ==}
|
||||||
|
|
||||||
|
'@supabase/realtime-js@2.9.5':
|
||||||
|
resolution: {integrity: sha512-TEHlGwNGGmKPdeMtca1lFTYCedrhTAv3nZVoSjrKQ+wkMmaERuCe57zkC5KSWFzLYkb5FVHW8Hrr+PX1DDwplQ==}
|
||||||
|
|
||||||
|
'@supabase/storage-js@2.5.5':
|
||||||
|
resolution: {integrity: sha512-OpLoDRjFwClwc2cjTJZG8XviTiQH4Ik8sCiMK5v7et0MDu2QlXjCAW3ljxJB5+z/KazdMOTnySi+hysxWUPu3w==}
|
||||||
|
|
||||||
|
'@supabase/supabase-js@2.43.0':
|
||||||
|
resolution: {integrity: sha512-e/MXc/7HPEXayWoxDTv+xqxF5+wlpH3/+UoP3TXLH0OiShICTbyJfmRgn/GCNw7E7+DlUq+3c48q12FtGWrVmQ==}
|
||||||
|
|
||||||
'@swc/helpers@0.5.11':
|
'@swc/helpers@0.5.11':
|
||||||
resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==}
|
resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==}
|
||||||
|
|
||||||
@@ -1153,6 +1181,9 @@ packages:
|
|||||||
'@types/normalize-package-data@2.4.4':
|
'@types/normalize-package-data@2.4.4':
|
||||||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
||||||
|
|
||||||
|
'@types/phoenix@1.6.4':
|
||||||
|
resolution: {integrity: sha512-B34A7uot1Cv0XtaHRYDATltAdKx0BvVKNgYNqE4WjtPUa4VQJM7kxeXcVKaH+KS+kCmZ+6w+QaUdcljiheiBJA==}
|
||||||
|
|
||||||
'@types/resolve@1.20.2':
|
'@types/resolve@1.20.2':
|
||||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||||
|
|
||||||
@@ -1162,6 +1193,9 @@ packages:
|
|||||||
'@types/web-bluetooth@0.0.20':
|
'@types/web-bluetooth@0.0.20':
|
||||||
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
||||||
|
|
||||||
|
'@types/ws@8.5.10':
|
||||||
|
resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@7.8.0':
|
'@typescript-eslint/eslint-plugin@7.8.0':
|
||||||
resolution: {integrity: sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==}
|
resolution: {integrity: sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==}
|
||||||
engines: {node: ^18.18.0 || >=20.0.0}
|
engines: {node: ^18.18.0 || >=20.0.0}
|
||||||
@@ -5600,6 +5634,18 @@ snapshots:
|
|||||||
- rollup
|
- rollup
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@nuxtjs/supabase@1.2.2(rollup@4.17.2)':
|
||||||
|
dependencies:
|
||||||
|
'@nuxt/kit': 3.11.2(rollup@4.17.2)
|
||||||
|
'@supabase/supabase-js': 2.43.0
|
||||||
|
defu: 6.1.4
|
||||||
|
pathe: 1.1.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- rollup
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
'@nuxtjs/tailwindcss@6.12.0(rollup@4.17.2)':
|
'@nuxtjs/tailwindcss@6.12.0(rollup@4.17.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/kit': 3.11.2(rollup@4.17.2)
|
'@nuxt/kit': 3.11.2(rollup@4.17.2)
|
||||||
@@ -5987,6 +6033,48 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
|
'@supabase/auth-js@2.64.1':
|
||||||
|
dependencies:
|
||||||
|
'@supabase/node-fetch': 2.6.15
|
||||||
|
|
||||||
|
'@supabase/functions-js@2.3.1':
|
||||||
|
dependencies:
|
||||||
|
'@supabase/node-fetch': 2.6.15
|
||||||
|
|
||||||
|
'@supabase/node-fetch@2.6.15':
|
||||||
|
dependencies:
|
||||||
|
whatwg-url: 5.0.0
|
||||||
|
|
||||||
|
'@supabase/postgrest-js@1.15.2':
|
||||||
|
dependencies:
|
||||||
|
'@supabase/node-fetch': 2.6.15
|
||||||
|
|
||||||
|
'@supabase/realtime-js@2.9.5':
|
||||||
|
dependencies:
|
||||||
|
'@supabase/node-fetch': 2.6.15
|
||||||
|
'@types/phoenix': 1.6.4
|
||||||
|
'@types/ws': 8.5.10
|
||||||
|
ws: 8.17.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
'@supabase/storage-js@2.5.5':
|
||||||
|
dependencies:
|
||||||
|
'@supabase/node-fetch': 2.6.15
|
||||||
|
|
||||||
|
'@supabase/supabase-js@2.43.0':
|
||||||
|
dependencies:
|
||||||
|
'@supabase/auth-js': 2.64.1
|
||||||
|
'@supabase/functions-js': 2.3.1
|
||||||
|
'@supabase/node-fetch': 2.6.15
|
||||||
|
'@supabase/postgrest-js': 1.15.2
|
||||||
|
'@supabase/realtime-js': 2.9.5
|
||||||
|
'@supabase/storage-js': 2.5.5
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
'@swc/helpers@0.5.11':
|
'@swc/helpers@0.5.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
@@ -6026,12 +6114,18 @@ snapshots:
|
|||||||
|
|
||||||
'@types/normalize-package-data@2.4.4': {}
|
'@types/normalize-package-data@2.4.4': {}
|
||||||
|
|
||||||
|
'@types/phoenix@1.6.4': {}
|
||||||
|
|
||||||
'@types/resolve@1.20.2': {}
|
'@types/resolve@1.20.2': {}
|
||||||
|
|
||||||
'@types/semver@7.5.8': {}
|
'@types/semver@7.5.8': {}
|
||||||
|
|
||||||
'@types/web-bluetooth@0.0.20': {}
|
'@types/web-bluetooth@0.0.20': {}
|
||||||
|
|
||||||
|
'@types/ws@8.5.10':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.12.11
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@7.8.0(@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)':
|
'@typescript-eslint/eslint-plugin@7.8.0(@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.10.0
|
'@eslint-community/regexpp': 4.10.0
|
||||||
|
|||||||
128
supabase/config.toml
Normal file
128
supabase/config.toml
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||||
|
# working directory name when running `supabase init`.
|
||||||
|
project_id = "leim-tools"
|
||||||
|
|
||||||
|
[api]
|
||||||
|
enabled = true
|
||||||
|
# Port to use for the API URL.
|
||||||
|
port = 54321
|
||||||
|
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||||
|
# endpoints. `public` is always included.
|
||||||
|
schemas = ["public", "graphql_public"]
|
||||||
|
# Extra schemas to add to the search_path of every request. `public` is always included.
|
||||||
|
extra_search_path = ["public", "extensions"]
|
||||||
|
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
|
||||||
|
# for accidental or malicious requests.
|
||||||
|
max_rows = 1000
|
||||||
|
|
||||||
|
[db]
|
||||||
|
# Port to use for the local database URL.
|
||||||
|
port = 54322
|
||||||
|
# Port used by db diff command to initialize the shadow database.
|
||||||
|
shadow_port = 54320
|
||||||
|
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
||||||
|
# server_version;` on the remote database to check.
|
||||||
|
major_version = 15
|
||||||
|
|
||||||
|
[db.pooler]
|
||||||
|
enabled = false
|
||||||
|
# Port to use for the local connection pooler.
|
||||||
|
port = 54329
|
||||||
|
# Specifies when a server connection can be reused by other clients.
|
||||||
|
# Configure one of the supported pooler modes: `transaction`, `session`.
|
||||||
|
pool_mode = "transaction"
|
||||||
|
# How many server connections to allow per user/database pair.
|
||||||
|
default_pool_size = 20
|
||||||
|
# Maximum number of client connections allowed.
|
||||||
|
max_client_conn = 100
|
||||||
|
|
||||||
|
[realtime]
|
||||||
|
enabled = true
|
||||||
|
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
|
||||||
|
# ip_version = "IPv6"
|
||||||
|
# The maximum length in bytes of HTTP request headers. (default: 4096)
|
||||||
|
# max_header_length = 4096
|
||||||
|
|
||||||
|
[studio]
|
||||||
|
enabled = true
|
||||||
|
# Port to use for Supabase Studio.
|
||||||
|
port = 54323
|
||||||
|
# External URL of the API server that frontend connects to.
|
||||||
|
api_url = "http://127.0.0.1"
|
||||||
|
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
|
||||||
|
openai_api_key = "env(OPENAI_API_KEY)"
|
||||||
|
|
||||||
|
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
|
||||||
|
# are monitored, and you can view the emails that would have been sent from the web interface.
|
||||||
|
[inbucket]
|
||||||
|
enabled = true
|
||||||
|
# Port to use for the email testing server web interface.
|
||||||
|
port = 54324
|
||||||
|
# Uncomment to expose additional ports for testing user applications that send emails.
|
||||||
|
# smtp_port = 54325
|
||||||
|
# pop3_port = 54326
|
||||||
|
|
||||||
|
[storage]
|
||||||
|
enabled = true
|
||||||
|
# The maximum file size allowed (e.g. "5MB", "500KB").
|
||||||
|
file_size_limit = "50MiB"
|
||||||
|
|
||||||
|
[storage.image_transformation]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[auth]
|
||||||
|
enabled = true
|
||||||
|
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||||
|
# in emails.
|
||||||
|
site_url = "http://localhost:5173/"
|
||||||
|
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||||
|
additional_redirect_urls = ["http://localhost:5173/"]
|
||||||
|
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||||
|
jwt_expiry = 3600
|
||||||
|
# If disabled, the refresh token will never expire.
|
||||||
|
enable_refresh_token_rotation = true
|
||||||
|
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
|
||||||
|
# Requires enable_refresh_token_rotation = true.
|
||||||
|
refresh_token_reuse_interval = 10
|
||||||
|
# Allow/disallow new user signups to your project.
|
||||||
|
enable_signup = true
|
||||||
|
# Allow/disallow anonymous sign-ins to your project.
|
||||||
|
enable_anonymous_sign_ins = false
|
||||||
|
# Allow/disallow testing manual linking of accounts
|
||||||
|
enable_manual_linking = false
|
||||||
|
|
||||||
|
[auth.email]
|
||||||
|
# Allow/disallow new user signups via email to your project.
|
||||||
|
enable_signup = false
|
||||||
|
# If enabled, a user will be required to confirm any email change on both the old, and new email
|
||||||
|
# addresses. If disabled, only the new email is required to confirm.
|
||||||
|
double_confirm_changes = false
|
||||||
|
# If enabled, users need to confirm their email address before signing in.
|
||||||
|
enable_confirmations = false
|
||||||
|
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
|
||||||
|
max_frequency = "1s"
|
||||||
|
|
||||||
|
# Uncomment to customize email template
|
||||||
|
# [auth.email.template.invite]
|
||||||
|
# subject = "You have been invited"
|
||||||
|
# content_path = "./supabase/templates/invite.html"
|
||||||
|
|
||||||
|
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
|
||||||
|
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
|
||||||
|
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
|
||||||
|
[auth.external.google]
|
||||||
|
enabled = true
|
||||||
|
client_id = "env(GOOGLE_AUTH_ID)"
|
||||||
|
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
|
||||||
|
secret = "env(GOOGLE_AUTH_SECRET)"
|
||||||
|
# Overrides the default auth redirectUrl.
|
||||||
|
redirect_uri = ""
|
||||||
|
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
|
||||||
|
# or any other third-party OIDC providers.
|
||||||
|
url = ""
|
||||||
|
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
|
||||||
|
skip_nonce_check = false
|
||||||
|
|
||||||
|
[auth.hook.custom_access_token]
|
||||||
|
enabled = true
|
||||||
|
uri = "pg-functions://postgres/public/custom_access_token_hook"
|
||||||
103
supabase/migrations/202401_init.sql
Normal file
103
supabase/migrations/202401_init.sql
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
--
|
||||||
|
-- For use with https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone
|
||||||
|
--
|
||||||
|
|
||||||
|
-- Custom types
|
||||||
|
create type public.app_permission as enum ('events.see.hidden');
|
||||||
|
create type public.app_role as enum ('sa', 'gm');
|
||||||
|
|
||||||
|
-- USERS
|
||||||
|
create table public.users (
|
||||||
|
id uuid references auth.users not null primary key, -- UUID from auth.users
|
||||||
|
username text
|
||||||
|
);
|
||||||
|
comment on table public.users is 'Profile data for each user.';
|
||||||
|
comment on column public.users.id is 'References the internal Supabase Auth user.';
|
||||||
|
|
||||||
|
-- USER ROLES
|
||||||
|
create table public.user_roles (
|
||||||
|
id bigint generated by default as identity primary key,
|
||||||
|
user_id uuid references public.users on delete cascade not null,
|
||||||
|
role app_role not null,
|
||||||
|
unique (user_id, role)
|
||||||
|
);
|
||||||
|
comment on table public.user_roles is 'Application roles for each user.';
|
||||||
|
|
||||||
|
-- ROLE PERMISSIONS
|
||||||
|
create table public.role_permissions (
|
||||||
|
id bigint generated by default as identity primary key,
|
||||||
|
role app_role not null,
|
||||||
|
permission app_permission not null,
|
||||||
|
unique (role, permission)
|
||||||
|
);
|
||||||
|
comment on table public.role_permissions is 'Application permissions for each role.';
|
||||||
|
|
||||||
|
-- authorize with role-based access control (RBAC)
|
||||||
|
create function public.authorize(
|
||||||
|
requested_permission app_permission
|
||||||
|
)
|
||||||
|
returns boolean as $$
|
||||||
|
declare
|
||||||
|
bind_permissions int;
|
||||||
|
begin
|
||||||
|
select count(*)
|
||||||
|
from public.role_permissions
|
||||||
|
where role_permissions.permission = authorize.requested_permission
|
||||||
|
and role_permissions.role = (auth.jwt() ->> 'user_role')::public.app_role
|
||||||
|
into bind_permissions;
|
||||||
|
|
||||||
|
return bind_permissions > 0;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql security definer set search_path = public;
|
||||||
|
|
||||||
|
-- Secure the tables
|
||||||
|
alter table public.users enable row level security;
|
||||||
|
alter table public.user_roles enable row level security;
|
||||||
|
alter table public.role_permissions enable row level security;
|
||||||
|
create policy "Allow logged-in read access" on public.users for select using ( auth.role() = 'authenticated' );
|
||||||
|
create policy "Allow individual insert access" on public.users for insert with check ( auth.uid() = id );
|
||||||
|
create policy "Allow individual update access" on public.users for update using ( auth.uid() = id );
|
||||||
|
create policy "Allow individual read access" on public.user_roles for select using ( auth.uid() = user_id );
|
||||||
|
|
||||||
|
-- Send "previous data" on change
|
||||||
|
alter table public.users replica identity full;
|
||||||
|
|
||||||
|
-- inserts a row into public.users and assigns roles
|
||||||
|
create function public.handle_new_user()
|
||||||
|
returns trigger as $$
|
||||||
|
declare is_admin boolean;
|
||||||
|
begin
|
||||||
|
insert into public.users (id, username)
|
||||||
|
values (new.id, new.email);
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql security definer set search_path = auth, public;
|
||||||
|
|
||||||
|
-- trigger the function every time a user is created
|
||||||
|
create trigger on_auth_user_created
|
||||||
|
after insert on auth.users
|
||||||
|
for each row execute procedure public.handle_new_user();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HELPER FUNCTIONS
|
||||||
|
* Create test user helper method.
|
||||||
|
*/
|
||||||
|
create or replace function public.create_user(
|
||||||
|
email text
|
||||||
|
) returns uuid
|
||||||
|
security definer
|
||||||
|
set search_path = auth
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
user_id uuid;
|
||||||
|
begin
|
||||||
|
user_id := extensions.uuid_generate_v4();
|
||||||
|
|
||||||
|
insert into auth.users (id, email)
|
||||||
|
values (user_id, email)
|
||||||
|
returning id into user_id;
|
||||||
|
|
||||||
|
return user_id;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
||||||
58
supabase/migrations/202402_auth_hook.sql
Normal file
58
supabase/migrations/202402_auth_hook.sql
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* AUTH HOOKS
|
||||||
|
* Create an auth hook to add a custom claim to the access token jwt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- Create the auth hook function
|
||||||
|
-- https://supabase.com/docs/guides/auth/auth-hooks#hook-custom-access-token
|
||||||
|
create or replace function public.custom_access_token_hook(event jsonb)
|
||||||
|
returns jsonb
|
||||||
|
language plpgsql
|
||||||
|
stable
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
claims jsonb;
|
||||||
|
user_role public.app_role;
|
||||||
|
begin
|
||||||
|
-- Check if the user is marked as admin in the profiles table
|
||||||
|
select role into user_role from public.user_roles where user_id = (event->>'user_id')::uuid;
|
||||||
|
|
||||||
|
claims := event->'claims';
|
||||||
|
|
||||||
|
if user_role is not null then
|
||||||
|
-- Set the claim
|
||||||
|
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
|
||||||
|
else
|
||||||
|
claims := jsonb_set(claims, '{user_role}', 'null');
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- Update the 'claims' object in the original event
|
||||||
|
event := jsonb_set(event, '{claims}', claims);
|
||||||
|
|
||||||
|
-- Return the modified or original event
|
||||||
|
return event;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
grant usage on schema public to supabase_auth_admin;
|
||||||
|
|
||||||
|
grant execute
|
||||||
|
on function public.custom_access_token_hook
|
||||||
|
to supabase_auth_admin;
|
||||||
|
|
||||||
|
revoke execute
|
||||||
|
on function public.custom_access_token_hook
|
||||||
|
from authenticated, anon;
|
||||||
|
|
||||||
|
grant all
|
||||||
|
on table public.user_roles
|
||||||
|
to supabase_auth_admin;
|
||||||
|
|
||||||
|
revoke all
|
||||||
|
on table public.user_roles
|
||||||
|
from authenticated, anon;
|
||||||
|
|
||||||
|
create policy "Allow auth admin to read user roles" ON public.user_roles
|
||||||
|
as permissive for select
|
||||||
|
to supabase_auth_admin
|
||||||
|
using (true)
|
||||||
1
supabase/seed.sql
Normal file
1
supabase/seed.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
insert into public.role_permissions (role, permission) values ('gm', 'events.see.hidden');
|
||||||
Reference in New Issue
Block a user