Skip to content

Commit 7c16172

Browse files
committed
feat: rudimentary login page
1 parent 043bc14 commit 7c16172

File tree

7 files changed

+253
-84
lines changed

7 files changed

+253
-84
lines changed

middleware/auth.ts

+3-9
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
import { storeToRefs } from "pinia";
22

33
export default defineNuxtRouteMiddleware((to) => {
4-
const { authenticated } = storeToRefs(useAuthStore());
5-
const token = useCookie("token");
4+
const { user } = storeToRefs(useAuthStore());
65

7-
if (token.value) authenticated.value = true;
6+
console.log(user.value);
87

9-
if (token.value && to.name === "login") return navigateTo("/");
10-
11-
if (!token.value && to.name !== "login") {
12-
abortNavigation();
13-
return navigateTo("/login");
14-
}
8+
if (to.fullPath === "/login" && user.value.id) return navigateTo("/");
159
});

nuxt.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ export default defineNuxtConfig({
1616
},
1717

1818
css: ['@wale/general-sans', '~/assets/css/main.css'],
19-
modules: ["@nuxt/image"]
19+
modules: ["@nuxt/image", "@pinia/nuxt"]
2020
});

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"db:format": "prisma format"
1717
},
1818
"dependencies": {
19+
"@pinia/nuxt": "^0.5.1",
1920
"@prisma/client": "5.9.1",
2021
"@wale/general-sans": "^1.0.0",
2122
"argon2": "^0.40.1",

pages/login.vue

+99-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,112 @@
11
<template>
22
<Layout title="Login | Readconquista" description="Login to Readconquista">
33
<div
4-
class="flex h-full items-center justify-center flex-col gap-4 md:gap-8 pt-24"
4+
class="flex h-full items-center justify-center flex-col gap-4 md:gap-8"
55
>
6-
<h2 class="text-xl md:text-2xl font-semibold">Login</h2>
6+
<h2 class="text-3xl md:text-5xl font-black">Login</h2>
7+
<h3
8+
v-if="formIsEmail === true"
9+
class="underline underline-offset-4 decoration-grayscale-800 font-light text-lg md:text-xl text-grayscale-800 cursor-pointer"
10+
@click="formIsEmail = !formIsEmail"
11+
>
12+
Using a username?
13+
</h3>
14+
<h3
15+
v-else
16+
class="underline underline-offset-4 decoration-grayscale-800 font-light text-lg text-grayscale-800 cursor-pointer"
17+
@click="formIsEmail = !formIsEmail"
18+
>
19+
Using an email address?
20+
</h3>
21+
<form class="flex flex-col gap-4" @submit.prevent="login()">
22+
<div>
23+
<input
24+
v-if="formIsEmail === true"
25+
v-model="email"
26+
type="email"
27+
class="rounded-lg resize-none w-full block bg-grayscale-400 px-4 py-2 placeholder:justify-center"
28+
placeholder="Email address"
29+
/>
30+
<input
31+
v-else
32+
v-model="username"
33+
class="rounded-lg resize-none w-full block bg-grayscale-400 px-4 py-2 placeholder:justify-center"
34+
placeholder="Username"
35+
/>
36+
</div>
37+
<div>
38+
<input
39+
v-model="password"
40+
type="password"
41+
class="rounded-lg resize-none w-full block bg-grayscale-400 px-4 py-2 placeholder:justify-center"
42+
placeholder="Password"
43+
/>
44+
</div>
45+
<div class="mt-4">
46+
<button
47+
v-if="formIsEmail === true"
48+
type="submit"
49+
class="rounded-xl w-full p-2"
50+
:disabled="!email || !password"
51+
:class="
52+
email && password
53+
? 'bg-grayscale-900 text-grayscale-400'
54+
: 'bg-grayscale-400 text-grayscale-900'
55+
"
56+
>
57+
Log In
58+
</button>
59+
<button
60+
v-else
61+
type="submit"
62+
class="rounded-xl w-full p-2"
63+
:disabled="!username || !password"
64+
:class="
65+
username && password
66+
? 'bg-grayscale-900 text-grayscale-400'
67+
: 'bg-grayscale-400 text-grayscale-900'
68+
"
69+
>
70+
Log In
71+
</button>
72+
</div>
73+
<div v-if="error" class="mt-2 flex flex-row">
74+
<span class="text-red-500 font-bold">{{ error }}</span>
75+
</div>
76+
</form>
777
</div>
878
</Layout>
979
</template>
1080

1181
<script setup lang="ts">
12-
import { storeToRefs } from "pinia";
82+
import { ref } from "vue";
1383
1484
import { useAuthStore } from "~/utils/authStore";
15-
import { pinia } from "~/utils/pinia";
85+
// import { pinia } from "~/utils/pinia";
1686
17-
const { authenticated } = storeToRefs(useAuthStore(pinia));
87+
const userStore = useAuthStore();
88+
89+
// Check if the user has selected username or email
90+
const formIsEmail = ref(false);
91+
92+
const email = ref("");
93+
const username = ref("");
94+
const password = ref("");
95+
96+
// I'm so fucking sorry.
97+
const error = ref();
98+
99+
const router = useRouter();
100+
101+
const login = async () => {
102+
const { data, requestState } = await userStore.login(
103+
password.value,
104+
username.value,
105+
email.value,
106+
);
107+
108+
if (requestState.error) error.value = requestState.error;
109+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
110+
else if (data) await router.push("/");
111+
};
18112
</script>

server/api/auth/login.post.ts

+71-34
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { type User } from "@prisma/client";
12
import * as argon2 from "argon2";
23
import { v4 } from "uuid";
34
import { z } from "zod";
@@ -20,47 +21,83 @@ export default defineEventHandler(async (event) => {
2021
// eslint-disable-next-line @typescript-eslint/no-throw-literal
2122
if (!result.success) throw result.error.issues;
2223

23-
const byUser = await db.user.findFirst({
24-
where: {
25-
OR: [
26-
{
27-
username: result.data.username,
28-
},
29-
{
30-
email: result.data.email,
31-
},
32-
],
33-
},
34-
});
24+
let byUser: User | null;
3525

36-
if (byUser === null)
37-
throw createError({
38-
statusCode: 403,
39-
statusMessage: "No user found with that email/username.",
26+
if (result.data.username === "" || result.data.username === undefined) {
27+
byUser = await db.user.findFirst({
28+
where: {
29+
email: result.data.email,
30+
},
4031
});
41-
else {
42-
// Check if the password matches
43-
const validPassword = await argon2.verify(
44-
byUser.password,
45-
result.data.password,
46-
);
4732

48-
if (!validPassword)
33+
if (byUser === null)
4934
throw createError({
5035
statusCode: 403,
51-
statusMessage: "Invalid password.",
36+
statusMessage: "No user found with that email/username.",
5237
});
38+
else {
39+
// Check if the password matches
40+
const validPassword = await argon2.verify(
41+
byUser.password,
42+
result.data.password,
43+
);
5344

54-
const jti = v4();
55-
const { accessToken, refreshToken } = generateTokens(byUser, jti);
56-
await addRefreshToken(jti, refreshToken, byUser);
45+
if (!validPassword)
46+
throw createError({
47+
statusCode: 403,
48+
statusMessage: "Invalid password.",
49+
});
5750

58-
return {
59-
id: byUser.id,
60-
username: byUser.username,
61-
email: byUser.email,
62-
accessToken,
63-
refreshToken,
64-
};
51+
const jti = v4();
52+
const { accessToken, refreshToken } = generateTokens(byUser, jti);
53+
await addRefreshToken(jti, refreshToken, byUser);
54+
55+
return {
56+
id: byUser.id,
57+
username: byUser.username,
58+
email: byUser.email,
59+
accessToken,
60+
refreshToken,
61+
};
62+
}
63+
}
64+
65+
if (result.data.email === "" || result.data.email === undefined) {
66+
byUser = await db.user.findFirst({
67+
where: {
68+
username: result.data.username,
69+
},
70+
});
71+
72+
if (byUser === null)
73+
throw createError({
74+
statusCode: 403,
75+
statusMessage: "No user found with that email/username.",
76+
});
77+
else {
78+
// Check if the password matches
79+
const validPassword = await argon2.verify(
80+
byUser.password,
81+
result.data.password,
82+
);
83+
84+
if (!validPassword)
85+
throw createError({
86+
statusCode: 403,
87+
statusMessage: "Invalid password.",
88+
});
89+
90+
const jti = v4();
91+
const { accessToken, refreshToken } = generateTokens(byUser, jti);
92+
await addRefreshToken(jti, refreshToken, byUser);
93+
94+
return {
95+
id: byUser.id,
96+
username: byUser.username,
97+
email: byUser.email,
98+
accessToken,
99+
refreshToken,
100+
};
101+
}
65102
}
66103
});

utils/authStore.ts

+64-30
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,76 @@
11
import { defineStore } from "pinia";
22

3+
interface RequestState {
4+
loading: boolean;
5+
error: Error | null;
6+
}
7+
8+
interface UserState {
9+
id: string;
10+
username: string;
11+
email: string;
12+
accessToken: string;
13+
refreshToken: string;
14+
}
15+
316
export const useAuthStore = defineStore("auth", {
417
state: () => ({
5-
accessToken: "",
6-
refreshToken: "",
7-
username: "",
8-
email: "",
9-
authenticated: false,
18+
user: {} as UserState,
1019
}),
1120
actions: {
12-
async login(password: string, username?: string, email?: string) {
13-
const { data } = await useFetch("/api/auth/login", {
14-
method: "post",
15-
headers: { "Content-Type": "application/json" },
16-
body: {
17-
password,
18-
username,
19-
email,
20-
},
21-
});
22-
23-
if (data.value) {
24-
this.$state.accessToken = data.value.accessToken;
25-
this.$state.refreshToken = data.value.refreshToken;
26-
this.$state.username = data.value.username;
27-
this.$state.email = data.value.email;
28-
29-
this.authenticated = true;
30-
}
21+
async login(
22+
password: string,
23+
username?: string,
24+
email?: string,
25+
): Promise<{ data: UserState; requestState: RequestState }> {
26+
const requestState: RequestState = {
27+
loading: true,
28+
error: null,
29+
};
30+
31+
if (username === "")
32+
try {
33+
const data: UserState = await $fetch("/api/auth/login", {
34+
method: "post",
35+
headers: { "Content-Type": "application/json" },
36+
body: {
37+
password,
38+
email,
39+
},
40+
});
41+
requestState.loading = false;
42+
return { data, requestState };
43+
} catch (error) {
44+
requestState.loading = false;
45+
requestState.error = error as Error;
46+
return { data: {} as UserState, requestState };
47+
}
48+
49+
if (email === "")
50+
try {
51+
const data: UserState = await $fetch("/api/auth/login", {
52+
method: "post",
53+
headers: { "Content-Type": "application/json" },
54+
body: {
55+
username,
56+
password,
57+
},
58+
});
59+
60+
requestState.loading = false;
61+
return { data, requestState };
62+
} catch (error) {
63+
requestState.loading = false;
64+
requestState.error = error as Error;
65+
return { data: {} as UserState, requestState };
66+
}
67+
return { data: {} as UserState, requestState };
3168
},
69+
3270
async logout() {
33-
await revokeTokensByIdentifier({ email: this.$state.email });
71+
await revokeTokensByIdentifier({ email: this.user.email });
3472

35-
this.$state.accessToken = "";
36-
this.$state.refreshToken = "";
37-
this.$state.username = "";
38-
this.$state.email = "";
39-
this.authenticated = false;
73+
this.user = {} as UserState;
4074
},
4175
},
4276
});

0 commit comments

Comments
 (0)