User can signup / signin (WIP)

This commit is contained in:
ChatonDeAru
2024-09-14 01:40:55 +02:00
committed by ChatonDeAru (Romain)
parent c35de52aec
commit 37b2238b84
16 changed files with 663 additions and 365 deletions

View File

@@ -1,6 +1,10 @@
# Nuxt 3 Minimal Starter
[![Netlify Status](https://api.netlify.com/api/v1/badges/bcf66326-b808-4ffd-9018-fb24315fce5f/deploy-status)](https://app.netlify.com/sites/force-orange/deploys)
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
# Nuxt
Look at the
[Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to
learn more.
## Setup
@@ -72,4 +76,6 @@ yarn preview
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
Check out the
[deployment documentation](https://nuxt.com/docs/getting-started/deployment) for
more information.

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import type { Database } from '@pel/supabase/types'
defineOptions({
name: 'FOHeader',
})
const links = [{
label: 'Les actus',
to: '/'
}, {
label: 'Rejoindre FO',
to: '/join'
}]
const user = useSupabaseUser()
const { auth } = useSupabaseClient<Database>()
const signOut = async () => {
const { error } = await auth.signOut()
if (error) console.log(error)
}
</script>
<template>
<UHeader :links="links" class="bg-white dark:bg-gray-900 rounded-xl shadow-lg mx-4 mt-4">
<template #left>
<!-- <p class="font-logo text-orange-500 stroke-5 stroke-black-500 text-hlogo">Force Orange</p> -->
<NuxtImg src="/assets/img/logo-fo.svg" alt="Force Orange" />
</template>
<template #right>
<UColorModeButton />
{{ user?.user_metadata.firstname }}
<UButton v-if="user" @click="signOut" variant="soft">Se déconnecter</UButton>
<UButton v-else to="/signin" variant="soft">Se connecter</UButton>
</template>
</UHeader>
<UNotification v-if="user && !user?.email_confirmed_at" title="N'oubliez pas de confirmer votre adresse de courriel !"
:id="1" :timeout="0" />
</template>

View File

@@ -0,0 +1,41 @@
<script setup lang="ts">
defineOptions({
name: 'PasswordStrength',
})
const props = defineProps<{
modelValue: string | undefined
}>()
const emit = defineEmits(['update:modelValue'])
const data = useVModel(props, 'modelValue', emit)
const { strength } = usePasswordStrength(data)
const color = computed(() => {
if (strength.value.score === 1) return 'red'
if (strength.value.score === 2) return 'orange'
if (strength.value.score === 3) return 'yellow'
if (strength.value.score === 4) return 'green'
return 'blue'
})
</script>
<template>
<UMeter :value="strength.score" :max="4" :color="color">
<template #label>
<p class="text-sm font-thin">
<template v-if="strength.score === 1">
Le mot de passe est faible
</template>
<template v-else-if="strength.score === 2">
Le mot de passe est moyen
</template>
<template v-else-if="strength.score === 3">
Le mot de passe est bon
</template>
<template v-else-if="strength.score === 4">
Le mot de passe est fort
</template>
</p>
</template>
</UMeter>
</template>

View File

@@ -0,0 +1,42 @@
import { zxcvbn, zxcvbnOptions, type ZxcvbnResult } from '@zxcvbn-ts/core'
import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common'
import * as zxcvbnEnPackage from '@zxcvbn-ts/language-fr'
export const usePasswordStrength = (password: Ref<string | undefined>) => {
// 0 # too guessable: risky password. (guesses < 10 ^ 3)
// 1 # very guessable: protection from throttled online attacks. (guesses < 10 ^ 6)
// 2 # somewhat guessable: protection from unthrottled online attacks. (guesses < 10 ^ 8)
// 3 # safely unguessable: moderate protection from offline slow - hash scenario. (guesses < 10 ^ 10)
// 4 # very unguessable: strong protection from offline slow - hash scenario. (guesses >= 10 ^ 10)
const strength = ref<Partial<ZxcvbnResult>>({
score: 0,
})
const options = {
translations: zxcvbnEnPackage.translations,
graphs: zxcvbnCommonPackage.adjacencyGraphs,
dictionary: {
...zxcvbnCommonPackage.dictionary,
...zxcvbnEnPackage.dictionary,
},
}
zxcvbnOptions.setOptions(options)
watchDebounced(password, (newPassword) => {
if (!newPassword) {
strength.value = {
score: 0,
}
return
}
strength.value = zxcvbn(newPassword.trim())
}, { immediate: true, debounce: 500 })
return {
strength,
}
}

View File

@@ -1,30 +1,13 @@
<script setup lang="ts">
useHead({
bodyAttrs: {
class: 'bg-[#F9DD75F2]'
class: 'bg-orange-200 dark:bg-orange-900 dark:bg-opacity-30'
}
})
const links = [{
label: 'Les actus',
to: '/'
}, {
label: 'Rejoindre FO',
to: '/join'
}]
</script>
<template>
<UHeader :links="links" class="rounded-xl shadow-lg bg-white mx-4 mt-4">
<template #left>
<!-- <p class="font-logo text-orange-500 stroke-5 stroke-black-500 text-hlogo">Force Orange</p> -->
<NuxtImg src="/assets/img/logo-fo.svg" alt="Force Orange" />
</template>
<template #right>
<UColorModeButton />
<UButton to="/signin" variant="soft">Se connecter</UButton>
</template>
</UHeader>
<FOHeader />
<UMain class="m-4">
<slot />

View File

@@ -0,0 +1,7 @@
export default defineNuxtRouteMiddleware((to, _from) => {
const user = useSupabaseUser()
if (!user.value) {
return navigateTo('/signin')
}
})

View File

@@ -1,16 +1,16 @@
[build.environment]
NODE_VERSION = "21"
NODE_VERSION = "21"
[build]
publish = "dist"
command = "pnpm run build"
publish = ".output"
command = "pnpm run build"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
from = "/*"
to = "/index.html"
status = 200
[[headers]]
for = "/*"
[headers.values]
Access-Control-Allow-Origin = "*"
for = "/*"
[headers.values]
Access-Control-Allow-Origin = "*"

View File

@@ -33,7 +33,7 @@ export default defineNuxtConfig({
redirectOptions: {
login: '/signin',
callback: '/signin/confirm',
exclude: ['/*'],
exclude: ['/signin/*', '/join', '/public/*'],
},
cookieName: 'fo-cookies',
cookieOptions: {

View File

@@ -13,6 +13,10 @@
},
"dependencies": {
"@pel/shared": "workspace:*",
"@pel/supabase": "workspace:*",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-fr": "^3.0.2",
"nuxt": "^3.12.4",
"vue": "latest"
},

View File

@@ -1,14 +1,18 @@
<script setup lang="ts">
import { object, string, boolean, type InferType } from 'yup'
import type { Database } from '@pel/supabase/types'
import type { FormSubmitEvent } from '#ui/types'
definePageMeta({
name: 'Join',
})
const user = useSupabaseUser()
const { auth } = useSupabaseClient<Database>()
const schema = object({
mail: string().email('Invalid email').required('Required'),
password: string().required('Required'),
password: string().min(6).required('Required'),
firstname: string().required('Required'),
lastname: string().required('Required'),
isAdult: boolean().required('Required'),
@@ -16,8 +20,6 @@ const schema = object({
type Schema = InferType<typeof schema>
const strenght = ref(0)
const state = reactive({
mail: undefined,
password: undefined,
@@ -28,7 +30,25 @@ const state = reactive({
async function onSubmit(event: FormSubmitEvent<Schema>) {
// Do something with event.data
const formSubmit = event.data
console.log(event.data)
const { data, error } = await auth.signUp({
email: formSubmit.mail,
password: formSubmit.password,
options: {
data: {
firstname: formSubmit.firstname,
lastname: formSubmit.lastname,
is_adult: formSubmit.isAdult,
},
},
})
// TODO
// [x] create member in database if user logged in
if (error) console.log(error)
}
</script>
@@ -40,27 +60,21 @@ async function onSubmit(event: FormSubmitEvent<Schema>) {
</p>
<UForm :schema="schema" :state="state" @submit="onSubmit">
<UFormGroup label="Adresse courriel">
<UInput placeholder="Adresse courriel" name="mail" type="mail" v-model="state.mail" />
<UFormGroup label="Adresse courriel" eager-validation>
<UInput placeholder="Adresse courriel" name="mail" type="email" v-model="state.mail" />
</UFormGroup>
<UFormGroup label="Password">
<UFormGroup label="Password" eager-validation>
<UInput placeholder="Password" type="password" v-model="state.password" />
</UFormGroup>
<UMeter :value="strenght" :max="100">
<template #label="{ percent }">
<p class="text-sm font-thin">
le mot de passe est fort
</p>
</template>
</UMeter>
<PasswordStrength v-model="state.password" />
<div class="flex md:flex-row flex-col gap-4">
<UFormGroup label="Prénom" class="flex-1">
<UFormGroup label="Prénom" class="flex-1" eager-validation>
<UInput placeholder="Prénom" name="firstname" v-model="state.firstname" />
</UFormGroup>
<UFormGroup label="Nom de famille" class="flex-1">
<UFormGroup label="Nom de famille" class="flex-1" eager-validation>
<UInput placeholder="Nom de famille" name="lastname" v-model="state.lastname" />
</UFormGroup>
</div>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
import type { Database } from '@pel/supabase/types'
const user = useSupabaseUser()
const { auth } = useSupabaseClient<Database>()
watch(user, () => {
if (user.value)
return navigateTo('/')
}, { immediate: true })
onMounted(async () => {
const token_hash = window.location.hash.replace('#', '')
const type = 'signup'
const { error } = await auth.verifyOtp({ token_hash, type })
if (error) console.log(error)
})
</script>
<template>
<div>
<p class="u-text-black">
Redirecting...
</p>
</div>
</template>

View File

@@ -1,12 +1,49 @@
<script setup lang="ts">
import { object, string, type InferType } from 'yup'
import type { Database } from '@pel/supabase/types'
import type { FormSubmitEvent } from '#ui/types'
definePageMeta({
name: 'Signin',
})
const user = useSupabaseUser()
const { auth } = useSupabaseClient<Database>()
const schema = object({
mail: string().email('Invalid email').required('Required'),
password: string().required('Required'),
})
type Schema = InferType<typeof schema>
const state = reactive({
mail: undefined,
password: undefined,
})
async function onSignin(event: FormSubmitEvent<Schema>) {
const formSubmit = event.data
const { data, error } = await auth.signInWithPassword({
email: formSubmit.mail,
password: formSubmit.password
})
// TODO: add possibility to signin with magic link -> signInWithOtp
// const { data, error } = await auth.signInWithOtp({
// email: 'example@email.com',
// options: {
// // set this to false if you do not want the user to be automatically signed up
// shouldCreateUser: false,
// emailRedirectTo: 'https://example.com/welcome',
// },
// })
// TODO confirm signin
if (error) console.log(error)
}
</script>
<template>
@@ -14,7 +51,7 @@ const state = reactive({
<UCard>
<h1 class="text-2xl uppercase">Qui êtes-vous ?</h1>
<UForm :state="state">
<UForm :state="state" @submit="onSignin">
<UFormGroup label="Adresse courriel">
<UInput name="mail" v-model="state.mail" />
</UFormGroup>

View File

@@ -0,0 +1,7 @@
## Generate types from live database
`supabase gen types --lang=typescript --project-id YourProjectId > types/database.types.ts`
## Generate types when using local environment
`supabase gen types --lang=typescript --local > types/database.types.ts`

View File

@@ -0,0 +1,14 @@
{
"name": "@pel/supabase",
"type": "module",
"private": true,
"packageManager": "pnpm@9.9.0",
"main": "./type.ts",
"files": [
"types.ts"
],
"scripts": {
"lint": "eslint .",
"up": "taze major -I"
}
}

187
modules/supabase/types.ts Normal file
View File

@@ -0,0 +1,187 @@
// Generated types from supabase admin cli
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export type Database = {
public: {
Tables: {
profiles: {
Row: {
birthday: string | null
candidacy_message: string | null
created_at: string
description: string | null
discord_avatar_url: string | null
discord_id: string | null
displayname: string | null
firstname: string | null
id: string
is_adult: boolean
is_member: boolean
is_validated: boolean
last_validation_date: string | null
lastname: string | null
mail: string
pel_editions: number[]
phone_number: number | null
picture_url: string | null
updated_at: string | null
validation_comment: string | null
}
Insert: {
birthday?: string | null
candidacy_message?: string | null
created_at?: string
description?: string | null
discord_avatar_url?: string | null
discord_id?: string | null
displayname?: string | null
firstname?: string | null
id?: string
is_adult?: boolean
is_member?: boolean
is_validated?: boolean
last_validation_date?: string | null
lastname?: string | null
mail: string
pel_editions?: number[]
phone_number?: number | null
picture_url?: string | null
updated_at?: string | null
validation_comment?: string | null
}
Update: {
birthday?: string | null
candidacy_message?: string | null
created_at?: string
description?: string | null
discord_avatar_url?: string | null
discord_id?: string | null
displayname?: string | null
firstname?: string | null
id?: string
is_adult?: boolean
is_member?: boolean
is_validated?: boolean
last_validation_date?: string | null
lastname?: string | null
mail?: string
pel_editions?: number[]
phone_number?: number | null
picture_url?: string | null
updated_at?: string | null
validation_comment?: string | null
}
Relationships: [
{
foreignKeyName: "profiles_id_fkey"
columns: ["id"]
isOneToOne: true
referencedRelation: "users"
referencedColumns: ["id"]
},
]
}
}
Views: {
[_ in never]: never
}
Functions: {
[_ in never]: never
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
}
type PublicSchema = Database[Extract<keyof Database, "public">]
export type Tables<
PublicTableNameOrOptions extends
| keyof (PublicSchema["Tables"] & PublicSchema["Views"])
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
Database[PublicTableNameOrOptions["schema"]]["Views"])
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
Row: infer R
}
? R
: never
: PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] &
PublicSchema["Views"])
? (PublicSchema["Tables"] &
PublicSchema["Views"])[PublicTableNameOrOptions] extends {
Row: infer R
}
? R
: never
: never
export type TablesInsert<
PublicTableNameOrOptions extends
| keyof PublicSchema["Tables"]
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Insert: infer I
}
? I
: never
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
Insert: infer I
}
? I
: never
: never
export type TablesUpdate<
PublicTableNameOrOptions extends
| keyof PublicSchema["Tables"]
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Update: infer U
}
? U
: never
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
Update: infer U
}
? U
: never
: never
export type Enums<
PublicEnumNameOrOptions extends
| keyof PublicSchema["Enums"]
| { schema: keyof Database },
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
: never = never,
> = PublicEnumNameOrOptions extends { schema: keyof Database }
? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
: PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
? PublicSchema["Enums"][PublicEnumNameOrOptions]
: never