Skip to content

Commit

Permalink
feat: rudimentary login page
Browse files Browse the repository at this point in the history
  • Loading branch information
wale committed Mar 3, 2024
1 parent 043bc14 commit 7c16172
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 84 deletions.
12 changes: 3 additions & 9 deletions middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { storeToRefs } from "pinia";

export default defineNuxtRouteMiddleware((to) => {
const { authenticated } = storeToRefs(useAuthStore());
const token = useCookie("token");
const { user } = storeToRefs(useAuthStore());

if (token.value) authenticated.value = true;
console.log(user.value);

if (token.value && to.name === "login") return navigateTo("/");

if (!token.value && to.name !== "login") {
abortNavigation();
return navigateTo("/login");
}
if (to.fullPath === "/login" && user.value.id) return navigateTo("/");
});
2 changes: 1 addition & 1 deletion nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ export default defineNuxtConfig({
},

css: ['@wale/general-sans', '~/assets/css/main.css'],
modules: ["@nuxt/image"]
modules: ["@nuxt/image", "@pinia/nuxt"]
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"db:format": "prisma format"
},
"dependencies": {
"@pinia/nuxt": "^0.5.1",
"@prisma/client": "5.9.1",
"@wale/general-sans": "^1.0.0",
"argon2": "^0.40.1",
Expand Down
104 changes: 99 additions & 5 deletions pages/login.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,112 @@
<template>
<Layout title="Login | Readconquista" description="Login to Readconquista">
<div
class="flex h-full items-center justify-center flex-col gap-4 md:gap-8 pt-24"
class="flex h-full items-center justify-center flex-col gap-4 md:gap-8"
>
<h2 class="text-xl md:text-2xl font-semibold">Login</h2>
<h2 class="text-3xl md:text-5xl font-black">Login</h2>
<h3
v-if="formIsEmail === true"
class="underline underline-offset-4 decoration-grayscale-800 font-light text-lg md:text-xl text-grayscale-800 cursor-pointer"
@click="formIsEmail = !formIsEmail"
>
Using a username?
</h3>
<h3
v-else
class="underline underline-offset-4 decoration-grayscale-800 font-light text-lg text-grayscale-800 cursor-pointer"
@click="formIsEmail = !formIsEmail"
>
Using an email address?
</h3>
<form class="flex flex-col gap-4" @submit.prevent="login()">
<div>
<input
v-if="formIsEmail === true"
v-model="email"
type="email"
class="rounded-lg resize-none w-full block bg-grayscale-400 px-4 py-2 placeholder:justify-center"
placeholder="Email address"
/>
<input
v-else
v-model="username"
class="rounded-lg resize-none w-full block bg-grayscale-400 px-4 py-2 placeholder:justify-center"
placeholder="Username"
/>
</div>
<div>
<input
v-model="password"
type="password"
class="rounded-lg resize-none w-full block bg-grayscale-400 px-4 py-2 placeholder:justify-center"
placeholder="Password"
/>
</div>
<div class="mt-4">
<button
v-if="formIsEmail === true"
type="submit"
class="rounded-xl w-full p-2"
:disabled="!email || !password"
:class="
email && password
? 'bg-grayscale-900 text-grayscale-400'
: 'bg-grayscale-400 text-grayscale-900'
"
>
Log In
</button>
<button
v-else
type="submit"
class="rounded-xl w-full p-2"
:disabled="!username || !password"
:class="
username && password
? 'bg-grayscale-900 text-grayscale-400'
: 'bg-grayscale-400 text-grayscale-900'
"
>
Log In
</button>
</div>
<div v-if="error" class="mt-2 flex flex-row">
<span class="text-red-500 font-bold">{{ error }}</span>
</div>
</form>
</div>
</Layout>
</template>

<script setup lang="ts">
import { storeToRefs } from "pinia";
import { ref } from "vue";
import { useAuthStore } from "~/utils/authStore";
import { pinia } from "~/utils/pinia";
// import { pinia } from "~/utils/pinia";
const { authenticated } = storeToRefs(useAuthStore(pinia));
const userStore = useAuthStore();
// Check if the user has selected username or email
const formIsEmail = ref(false);
const email = ref("");
const username = ref("");
const password = ref("");
// I'm so fucking sorry.
const error = ref();
const router = useRouter();
const login = async () => {
const { data, requestState } = await userStore.login(
password.value,
username.value,
email.value,
);
if (requestState.error) error.value = requestState.error;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
else if (data) await router.push("/");
};
</script>
105 changes: 71 additions & 34 deletions server/api/auth/login.post.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { type User } from "@prisma/client";
import * as argon2 from "argon2";
import { v4 } from "uuid";
import { z } from "zod";
Expand All @@ -20,47 +21,83 @@ export default defineEventHandler(async (event) => {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
if (!result.success) throw result.error.issues;

const byUser = await db.user.findFirst({
where: {
OR: [
{
username: result.data.username,
},
{
email: result.data.email,
},
],
},
});
let byUser: User | null;

if (byUser === null)
throw createError({
statusCode: 403,
statusMessage: "No user found with that email/username.",
if (result.data.username === "" || result.data.username === undefined) {
byUser = await db.user.findFirst({
where: {
email: result.data.email,
},
});
else {
// Check if the password matches
const validPassword = await argon2.verify(
byUser.password,
result.data.password,
);

if (!validPassword)
if (byUser === null)
throw createError({
statusCode: 403,
statusMessage: "Invalid password.",
statusMessage: "No user found with that email/username.",
});
else {
// Check if the password matches
const validPassword = await argon2.verify(
byUser.password,
result.data.password,
);

const jti = v4();
const { accessToken, refreshToken } = generateTokens(byUser, jti);
await addRefreshToken(jti, refreshToken, byUser);
if (!validPassword)
throw createError({
statusCode: 403,
statusMessage: "Invalid password.",
});

return {
id: byUser.id,
username: byUser.username,
email: byUser.email,
accessToken,
refreshToken,
};
const jti = v4();
const { accessToken, refreshToken } = generateTokens(byUser, jti);
await addRefreshToken(jti, refreshToken, byUser);

return {
id: byUser.id,
username: byUser.username,
email: byUser.email,
accessToken,
refreshToken,
};
}
}

if (result.data.email === "" || result.data.email === undefined) {
byUser = await db.user.findFirst({
where: {
username: result.data.username,
},
});

if (byUser === null)
throw createError({
statusCode: 403,
statusMessage: "No user found with that email/username.",
});
else {
// Check if the password matches
const validPassword = await argon2.verify(
byUser.password,
result.data.password,
);

if (!validPassword)
throw createError({
statusCode: 403,
statusMessage: "Invalid password.",
});

const jti = v4();
const { accessToken, refreshToken } = generateTokens(byUser, jti);
await addRefreshToken(jti, refreshToken, byUser);

return {
id: byUser.id,
username: byUser.username,
email: byUser.email,
accessToken,
refreshToken,
};
}
}
});
94 changes: 64 additions & 30 deletions utils/authStore.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,76 @@
import { defineStore } from "pinia";

interface RequestState {
loading: boolean;
error: Error | null;
}

interface UserState {
id: string;
username: string;
email: string;
accessToken: string;
refreshToken: string;
}

export const useAuthStore = defineStore("auth", {
state: () => ({
accessToken: "",
refreshToken: "",
username: "",
email: "",
authenticated: false,
user: {} as UserState,
}),
actions: {
async login(password: string, username?: string, email?: string) {
const { data } = await useFetch("/api/auth/login", {
method: "post",
headers: { "Content-Type": "application/json" },
body: {
password,
username,
email,
},
});

if (data.value) {
this.$state.accessToken = data.value.accessToken;
this.$state.refreshToken = data.value.refreshToken;
this.$state.username = data.value.username;
this.$state.email = data.value.email;

this.authenticated = true;
}
async login(
password: string,
username?: string,
email?: string,
): Promise<{ data: UserState; requestState: RequestState }> {
const requestState: RequestState = {
loading: true,
error: null,
};

if (username === "")
try {
const data: UserState = await $fetch("/api/auth/login", {
method: "post",
headers: { "Content-Type": "application/json" },
body: {
password,
email,
},
});
requestState.loading = false;
return { data, requestState };
} catch (error) {
requestState.loading = false;
requestState.error = error as Error;
return { data: {} as UserState, requestState };
}

if (email === "")
try {
const data: UserState = await $fetch("/api/auth/login", {
method: "post",
headers: { "Content-Type": "application/json" },
body: {
username,
password,
},
});

requestState.loading = false;
return { data, requestState };
} catch (error) {
requestState.loading = false;
requestState.error = error as Error;
return { data: {} as UserState, requestState };
}
return { data: {} as UserState, requestState };
},

async logout() {
await revokeTokensByIdentifier({ email: this.$state.email });
await revokeTokensByIdentifier({ email: this.user.email });

this.$state.accessToken = "";
this.$state.refreshToken = "";
this.$state.username = "";
this.$state.email = "";
this.authenticated = false;
this.user = {} as UserState;
},
},
});
Loading

0 comments on commit 7c16172

Please sign in to comment.