Finished registration + working on own useAuth composable

This commit is contained in:
Peaceultime 2024-07-30 17:58:22 +02:00
parent 1d2a89e001
commit f2600a3012
23 changed files with 354 additions and 99 deletions

View File

@ -138,9 +138,6 @@ html.light-mode .light-block {
display: block;
}
.flex {
display: flex;
}
.align-baseline {
align-items: baseline;
}
@ -150,11 +147,26 @@ html.light-mode .light-block {
.align-stretch {
align-items: stretch;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.justify-evenly {
justify-content: space-evenly;
}
.justify-around {
justify-content: space-around;
}
@media screen and (max-width: 750px) {
.mobile-bigger {
flex: 3 1 0 !important;
}
.mobile-smaller {
flex: 1 3 0 !important;
}
.mobile-hidden {
display: none !important;
}
@ -163,10 +175,82 @@ html.light-mode .light-block {
}
}
@media screen and (min-width: 750px) {
.desktop-bigger {
flex: 3 1 0 !important;
}
.desktop-smaller {
flex: 1 3 0 !important;
}
.desktop-hidden {
display: none !important;
}
.desktop-block {
display: inherit !important;
}
}
.input-form.input-form-wide {
width: 600px;
}
.input-form {
width: 400px;
display: flex;
flex-direction: column;
padding: 0 2em 2em 2em;
border: 1px solid var(--background-modifier-border);
}
.input-group {
display: flex;
flex-direction: column;
padding: .5em;
}
.input-form h1 {
font-size: x-large;
}
.input-form button {
margin-top: 2em;
}
.input-group .input-label {
padding: 4px 1em;
}
.input-group .input-error {
padding: .5em 1em 4px;
color: var(--text-error);
user-select: text;
}
.input-group .input-input.input-has-error {
border-color: var(--text-error);
}
.password-validation-group {
display: flex;
flex-direction: column;
padding: .5em 2em;
}
.password-validation-title {
font-style: italic;
font-size: small;
padding-bottom: 4px;
}
.password-validation-item {
font-size: small;
padding-left: .5em;
}
.password-validation-item pre {
user-select: text;
}
.password-validation-item.validation-error {
color: var(--text-error);
}

View File

@ -2266,13 +2266,6 @@ button.mod-destructive {
color: var(--text-on-accent);
}
.input-label {
display: inline-block;
width: 150px;
text-align: right;
margin-right: var(--size-4-2);
}
.input-button {
padding: 6px 14px;
margin-left: 14px;

BIN
bun.lockb

Binary file not shown.

17
components/Input.vue Normal file
View File

@ -0,0 +1,17 @@
<script setup lang="ts">
interface Prop
{
error?: string | boolean;
title: string;
}
const props = defineProps<Prop>();
const model = defineModel<string>();
</script>
<template>
<div class="input-group">
<label v-if="title" class="input-label">{{ title }}</label>
<input class="input-input" :class="{'input-has-error': !!error}" v-model="model" v-bind="$attrs" />
<span v-if="error && typeof error === 'string'" class="input-error">{{ error }}</span>
</div>
</template>

54
composables/useAuth.ts Normal file
View File

@ -0,0 +1,54 @@
export enum AuthStatus
{
disconnected, loading, connected
};
export interface Auth
{
id: Ref<number>;
data: Ref<Record<string, any>>;
token: Ref<string>;
session_id: Ref<number>;
status: Ref<AuthStatus>;
lastRefresh: Ref<Date>;
register: (username: string, email: string, password: string, data?: Record<string, any>) => AuthStatus;
login: (usernameOrEmail: string, password: string) => AuthStatus;
logout: () => AuthStatus;
refresh: () => AuthStatus;
}
const id = useState<number>("auth:id", () => 0);
const data = useState<any>("auth:data", () => {});
const token = useState<string>("auth:token", () => '');
const session_id = useState<number>("auth:session_id", () => 0);
const status = useState<AuthStatus>("auth:status", () => 0);
const lastRefresh = useState<Date>("auth:date", () => new Date());
function register(username: string, email: string, password: string, data?: Record<string, any>): AuthStatus
{
return AuthStatus.disconnected;
}
function login(usernameOrEmail: string, password: string): AuthStatus
{
return AuthStatus.disconnected;
}
function logout(): AuthStatus
{
return AuthStatus.disconnected;
}
function refresh(): AuthStatus
{
return AuthStatus.disconnected;
}
export default function useAuth(): Auth {
return {
id, data, token, session_id, status, lastRefresh,
register, login, logout, refresh
};
}

View File

@ -0,0 +1,21 @@
import { Database } from "bun:sqlite";
let instance: Database | undefined;
export default function useDatabase(): Database {
if(instance === undefined)
instance = getDatabase();
return instance;
}
function getDatabase(): Database
{
const { dbFile } = useRuntimeConfig();
const db = new Database(dbFile);
db.exec("PRAGMA journal_mode = WAL;");
return db;
}

BIN
db.sqlite Normal file

Binary file not shown.

5
middleware/auth.ts Normal file
View File

@ -0,0 +1,5 @@
export default defineNuxtRouteMiddleware((to) => {
const meta = to.meta.auth;
//to.
})

View File

@ -2,45 +2,17 @@
import CanvasModule from './transformer/canvas/module'
export default defineNuxtConfig({
modules: [CanvasModule, "@nuxt/content", "@nuxtjs/color-mode", '@sidebase/nuxt-auth'],
modules: [CanvasModule, "@nuxt/content", "@nuxtjs/color-mode"],
css: ['~/assets/common.css', '~/assets/global.css'],
runtimeConfig: {
dbFile: ''
},
components: [
{
path: '~/components',
pathPrefix: false,
},
],
router: {
options: {
scrollBehaviorType: 'smooth'
}
},
auth: {
baseURL: '/api/auth',
provider: {
type: 'local',
//type: 'refresh',
endpoints: {
signIn: { path: '/login', method: 'post' },
signOut: { path: '/logout', method: 'post' },
signUp: { path: '/register', method: 'post' },
getSession: { path: '/session', method: 'get' },
//refresh: { path: '/refresh', method: 'post' }
},
session: {
dataType: {
id: 'string',
username: 'string',
email: 'string',
}
}
}
},
css: ['~/assets/common.css', '~/assets/global.css'],
content: {
ignores: [
'98.Privé'
@ -78,12 +50,5 @@ export default defineNuxtConfig({
}
}
},
vite: {
vue: {
customElement: ['Line', 'Circle', 'Path']
}
},
compatibilityDate: '2024-07-25'
})

View File

@ -2,7 +2,7 @@
"devDependencies": {
"@nuxt/content": "^2.13.2",
"@nuxtjs/color-mode": "^3.4.2",
"@sidebase/nuxt-auth": "^0.8.1",
"@types/bun": "^1.1.6",
"nuxt": "^3.12.4",
"vue": "^3.4.34",
"vue-router": "^4.4.0"
@ -14,6 +14,7 @@
"dependencies": {
"hast-util-to-html": "^9.0.1",
"remark-breaks": "^4.0.0",
"remark-ofm": "link:remark-ofm"
"remark-ofm": "link:remark-ofm",
"zod": "^3.23.8"
}
}

View File

@ -1,6 +1,5 @@
<script setup lang="ts">
definePageMeta({
title: '',
auth: {
unauthenticatedOnly: true,
navigateAuthenticatedTo: '/user/profile'

View File

@ -2,7 +2,7 @@
definePageMeta({
auth: {
unauthenticatedOnly: false,
navigateAuthenticatedTo: '/user/login'
navigateUnauthenticatedTo: '/user/login'
}
});

77
pages/user/register.vue Normal file
View File

@ -0,0 +1,77 @@
<script setup lang="ts">
import { schema, type Registration } from '~/schemas/registration';
definePageMeta({
auth: {
unauthenticatedOnly: true,
navigateAuthenticatedTo: '/user/profile'
}
});
const state = reactive<Registration>({
username: '',
email: '',
password: ''
});
const confirmPassword = ref("");
const { status, signUp } = useAuth();
const checkedLength = computed(() => state.password.length >= 8 && state.password.length <= 128);
const checkedLowerUpper = computed(() => state.password.toLowerCase() !== state.password && state.password.toUpperCase() !== state.password);
const checkedDigit = computed(() => /[0-9]/.test(state.password));
const checkedSymbol = computed(() => " !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => state.password.includes(e)));
const usernameError = ref("");
const emailError = ref("");
function register(): void
{
const data = schema.safeParse(state);
if(data.success && state.password !== "" && confirmPassword.value === state.password)
{
try {
signUp({ ...data.data }, { redirect: true, callbackUrl: '/' });
} catch(e) {
usernameError.value = e?.data?.find(e => e.path.includes("username"))?.message ?? "";
emailError.value = e?.data?.find(e => e.path.includes("email"))?.message ?? "";
}
}
else
{
usernameError.value = data.error?.issues.find(e => e.path.includes("username"))?.message ?? "";
emailError.value = data.error?.issues.find(e => e.path.includes("email"))?.message ?? "";
}
}
</script>
<template>
<Head>
<Title>S'inscrire</Title>
</Head>
<div class="site-body-center-column">
<div class="render-container flex align-center justify-center">
<form v-if="status === 'unauthenticated'" @submit.prevent="register" class="input-form input-form-wide">
<h1>Inscription</h1>
<Input type="text" v-model="state.username" placeholder="Entrez un nom d'utiliateur" title="Nom d'utilisateur" :error="usernameError"/>
<Input type="text" v-model="state.email" placeholder="Entrez une addresse mail" title="Adresse mail" :error="emailError"/>
<Input type="password" v-model="state.password" placeholder="Entrez un mot de passe" title="Mot de passe" :error="!(checkedLength && checkedLowerUpper && checkedDigit && checkedSymbol)"/>
<div class="password-validation-group">
<span class="password-validation-title">Votre mot de passe doit respecter les critères suivants :</span>
<span class="password-validation-item" :class="{'validation-error': !checkedLength}">Entre 8 et 128 caractères</span>
<span class="password-validation-item" :class="{'validation-error': !checkedLowerUpper}">Au moins une minuscule et une majuscule</span>
<span class="password-validation-item" :class="{'validation-error': !checkedDigit}">Au moins un chiffre</span>
<span class="password-validation-item" :class="{'validation-error': !checkedSymbol}">Au moins un caractère spécial parmis la liste suivante: <pre>! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ ] ^ _ ` { | } ~</pre></span>
</div>
<Input type="password" v-model="confirmPassword" placeholder="Confirmer le mot de passe" title="Confirmer le mot de passe" :error="confirmPassword === '' || confirmPassword === state.password ? '' : 'Les mots de passe saisies ne sont pas identique'"/>
<button>Valider</button>
</form>
<div v-else-if="status === 'loading'"></div>
<div v-else class="not-found-container">
<div class="not-found-title">👀 Vous n'avez rien à faire ici. 👀</div>
</div>
</div>
</div>
</template>

View File

@ -1,41 +0,0 @@
<script setup lang="ts">
definePageMeta({
title: '',
auth: {
unauthenticatedOnly: true,
navigateAuthenticatedTo: '/user/profile'
}
});
const username = ref<string>(), email = ref<string>(), password = ref<string>();
const { status, signUp } = useAuth();
function connect() {
signUp({ username, password, email }, { redirect: true, callbackUrl: '/' });
console.log(status.value);
}
</script>
<template>
<Head>
<Title>S'inscrire</Title>
</Head>
<!--<div class="site-body-center-column">
<div class="render-container">
<div v-if="status === 'unauthenticated'" class="not-found-container">
<form @submit.prevent="connect" class="column gapy-1">
<input type="text" :value="username" placeholder="Entrez un nom d'utiliateur">
<input type="text" :value="email" placeholder="Entrez une addresse mail">
<input type="password" :value="password" placeholder="Entrez un mot de passe">
<button>Valider</button>
</form>
</div>
<div v-else-if="status === 'loading'"></div>
<div v-else class="not-found-container">
<div class="not-found-title">👀 Vous n'avez rien à faire ici. 👀</div>
</div>
</div>
</div>-->
</template>

43
schemas/registration.ts Normal file
View File

@ -0,0 +1,43 @@
import { z } from "zod";
function securePassword(password: string, ctx: z.RefinementCtx): void {
const lowercase = password.toLowerCase();
const uppercase = password.toUpperCase();
if(lowercase === password)
{
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Votre mot de passe doit contenir au moins une majuscule",
});
}
if(uppercase === password)
{
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Votre mot de passe doit contenir au moins une minuscule",
});
}
if(!/[0-9]/.test(password))
{
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Votre mot de passe doit contenir au moins un chiffre",
});
}
if(!" !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => password.includes(e)))
{
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Votre mot de passe doit contenir au moins un symbole",
});
}
}
export const schema = z.object({
username: z.string({ required_error: "Nom d'utilisateur obligatoire" }).min(3, "Votre nom d'utilisateur doit contenir au moins 3 caractères").max(32, "Votre nom d'utilisateur doit contenir au plus 32 caractères"),
email: z.string({ required_error: "Email obligatoire" }).email("Adresse mail invalide"),
password: z.string({ required_error: "Mot de passe obligatoire" }).min(8, "Votre mot de passe doit contenir au moins 8 caractères").max(128, "Votre mot de passe doit contenir au moins 8 caractères").superRefine(securePassword),
});
export type Registration = z.infer<typeof schema>;

View File

@ -0,0 +1,2 @@
export default defineEventHandler(async (e) => {
});

View File

@ -0,0 +1,2 @@
export default defineEventHandler(async (e) => {
});

View File

@ -0,0 +1,35 @@
import useDatabase from '~/composables/useDatabase';
import { schema } from '~/schemas/registration';
export default defineEventHandler(async (e) => {
const body = await readValidatedBody(e, schema.safeParse);
if(!body.success)
return body.error;
const db = useDatabase();
const usernameQuery = db.query(`SELECT COUNT(*) as count FROM users WHERE username = ?1`);
const checkUsername = usernameQuery.get(body.data.username);
const emailQuery = db.query(`SELECT COUNT(*) as count FROM users WHERE email = ?1`);
const checkEmail = emailQuery.get(body.data.email);
const errors = [];
if(checkUsername.count !== 0)
errors.push({ path: ['username'], message: "Ce nom d'utilisateur est déjà utilisé" });
if(checkEmail.count !== 0)
errors.push({ path: ['email'], message: "Cette adresse mail est déjà utilisée" });
if(errors.length > 0)
throw createError({ status: 406, message: "duplicates", data: errors });
else
{
const hash = await Bun.password.hash(body.data.password);
const registration = db.query(`INSERT INTO users(username, email, hash) VALUES(?1, ?2, ?3)`);
const result = registration.get(body.data.username, body.data.email, hash);
setResponseStatus(e, 201, "Created");
return { success: true };
}
});

View File

@ -0,0 +1,2 @@
export default defineEventHandler(async (e) => {
});

View File

@ -1 +0,0 @@
export default defineEventHandler(() => 'Hello World!');

View File

@ -1 +0,0 @@
export default defineEventHandler(() => 'Hello World!');

View File

@ -1 +0,0 @@
export default defineEventHandler((...args) => console.log(...args));

View File

@ -1 +0,0 @@
export default defineEventHandler(() => 'Hello World!');