Skip to content

Commit

Permalink
Implemented user sign-in.
Browse files Browse the repository at this point in the history
  • Loading branch information
Utar94 committed Dec 20, 2023
1 parent 5275df2 commit 1c21a4e
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 5 deletions.
7 changes: 6 additions & 1 deletion src/api/account.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Actor } from "@/types/aggregate";
import type { RegisterPayload } from "@/types/account";
import type { RegisterPayload, SignInPayload } from "@/types/account";
import { useUserStore } from "@/stores/user";

export async function confirm(token: string): Promise<Actor | undefined> {
Expand All @@ -12,4 +12,9 @@ export async function register(payload: RegisterPayload): Promise<void> {
users.create(payload);
}

export async function signIn(payload: SignInPayload): Promise<Actor> {
const users = useUserStore();
return users.signIn(payload);
}

export async function signOut(): Promise<void> {}
38 changes: 35 additions & 3 deletions src/stores/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { nanoid } from "nanoid";
import { ref } from "vue";

import type { Actor } from "@/types/aggregate";
import type { RegisterPayload } from "@/types/account";
import type { RegisterPayload, SignInPayload } from "@/types/account";
import type { User } from "@/types/users";

function buildFullName(...names: (string | undefined)[]): string | undefined {
Expand Down Expand Up @@ -111,6 +111,38 @@ export const useUserStore = defineStore(
}
}

function signIn(payload: SignInPayload): Actor {
// Try finding the user by username
const usernameNormalized = payload.username.trim().toUpperCase();
let id: string | undefined = usernameIndex.value.get(usernameNormalized);

// Try finding the user by email address
if (!id) {
id = emailIndex.value.get(usernameNormalized);
}

// Checking credentials
if (!id) {
throw "INVALID_CREDENTIALS"; // TODO(fpion): use error object
}
const user: User | undefined = users.value.get(id);
if (!user || user.isDisabled) {
throw "INVALID_CREDENTIALS"; // TODO(fpion): use error object
}
if (user.hasPassword) {
const password: string | undefined = passwordHashes.value.get(id);
if (!password || !payload.password || hash(payload.password) !== password) {
throw "INVALID_CREDENTIALS"; // TODO(fpion): use error object
}
}

// Authenticate the user
user.authenticatedOn = new Date().toISOString();
users.value.set(id, user);

return toActor(user);
}

function verifyEmail(emailAddress: string): Actor | undefined {
// Find the user identifier using the email address
const emailAddressNormalized: string = emailAddress.trim().toUpperCase();
Expand Down Expand Up @@ -144,7 +176,7 @@ export const useUserStore = defineStore(
return actor;
}

return { create, verifyEmail };
return { create, signIn, verifyEmail };
},
{ persist: true },
{ persist: true }, // TODO(fpion): does not seem to work
);
6 changes: 6 additions & 0 deletions src/types/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ export type RegisterPayload = {
firstName?: string;
lastName?: string;
};

export type SignInPayload = {
username: string;
password?: string;
remember: boolean;
};
69 changes: 68 additions & 1 deletion src/views/account/SignInView.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,74 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import type { Actor } from "@/types/aggregate";
import type { SignInPayload } from "@/types/account";
import { signIn } from "@/api/account";
import { useAccountStore } from "@/stores/account";
const account = useAccountStore();
const route = useRoute();
const router = useRouter();
const invalidCredentials = ref<boolean>(false);
const loading = ref<boolean>(false);
const payload = ref<SignInPayload>({ username: "", remember: false });
async function submit(): Promise<void> {
if (!loading.value) {
loading.value = true;
invalidCredentials.value = false;
try {
const actor: Actor = await signIn(payload.value);
account.signIn(actor);
const redirect = route.query.redirect as string | undefined;
router.push(redirect ?? { name: "Profile" });
} catch (e) {
if (e === "INVALID_CREDENTIALS") {
invalidCredentials.value = true;
} else {
console.error(e); // TODO(fpion): error handling
}
} finally {
loading.value = false;
}
}
}
import { useUserStore } from "@/stores/user";
const users = useUserStore();
users.create({
username: "fpion",
});
</script>

<template>
<main class="container">
<h1>Sign In</h1>
<div v-if="invalidCredentials" class="alert alert-warning">Invalid Credentials!</div>
<form @submit.prevent="submit">
<div class="mb-3">
<label class="form-label" for="username">Username or Email Address <span class="text-danger">*</span></label>
<input class="form-control" id="username" placeholder="Username or Email Address" required type="text" v-model.trim="payload.username" />
</div>
<div class="mb-3">
<label class="form-label" for="password">Password</label>
<input class="form-control" id="password" placeholder="Password" type="password" v-model="payload.password" />
</div>
<div class="form-check mb-3">
<input class="form-check-input" id="remember-me" type="checkbox" v-model="payload.remember" />
<label class="form-check-label" for="remember-me">Remember me</label>
</div>
<button class="btn btn-primary" :disabled="loading" type="submit">
<span v-if="loading">
<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
<span class="visually-hidden">Loading...</span>
</span>
<FontAwesomeIcon v-else :icon="['fas', 'right-to-bracket']" />
Sign In
</button>
</form>
</main>
</template>

0 comments on commit 1c21a4e

Please sign in to comment.