Skip to content

Commit

Permalink
Fixed user storage.
Browse files Browse the repository at this point in the history
  • Loading branch information
Utar94 committed Dec 20, 2023
1 parent 5db5f95 commit cdafdab
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 136 deletions.
233 changes: 105 additions & 128 deletions src/stores/user.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { defineStore } from "pinia";
import { nanoid } from "nanoid";
import { ref } from "vue";

import UserRepository from "./userRepository";
import type { Actor } from "@/types/aggregate";
import type { ErrorDetail } from "@/types/api";
import type { RegisterPayload, SignInPayload } from "@/types/account";
import type { User } from "@/types/users";

Expand Down Expand Up @@ -35,148 +36,124 @@ function toActor(user: User): Actor {
};
}

export const useUserStore = defineStore(
"user",
() => {
const emailIndex = ref<Map<string, string>>(new Map<string, string>());
const passwordHashes = ref<Map<string, string>>(new Map<string, string>());
const usernameIndex = ref<Map<string, string>>(new Map<string, string>());
const users = ref<Map<string, User>>(new Map<string, User>());

function create(payload: RegisterPayload): void {
// Ensure username unicity
const username: string = payload.username.trim();
const usernameNormalized: string = username.toUpperCase();
if (usernameIndex.value.has(usernameNormalized)) {
return;
}
export const useUserStore = defineStore("user", () => {
function create(payload: RegisterPayload): void {
// Initialize storage
const users = new UserRepository(localStorage);

// Ensure email address unicity
const emailAddress: string | undefined = payload.emailAddress?.trim();
const emailAddressNormalized: string | undefined = emailAddress?.toUpperCase();
if (emailAddressNormalized && emailIndex.value.has(emailAddressNormalized)) {
return;
}
// Ensure username unicity
const username: string = payload.username.trim();
if (users.findByUsername(username)) {
return;
}

// Prepare user information
const firstName: string | undefined = payload.firstName?.trim();
const lastName: string | undefined = payload.lastName?.trim();
const fullName: string | undefined = buildFullName(firstName, lastName);

// Generate an unique identifier and a timestamp
const id: string = nanoid();
const now: string = new Date().toISOString();

// Create an actor
const actor: Actor = {
id,
type: "User",
isDeleted: false,
displayName: fullName ?? username,
emailAddress,
};

// Create the user
const user: User = {
id,
version: 1,
createdBy: actor,
createdOn: now,
updatedBy: actor,
updatedOn: now,
username,
hasPassword: Boolean(payload.password),
isDisabled: false,
isConfirmed: false,
firstName,
lastName,
fullName,
};
if (payload.password) {
user.passwordChangedBy = actor;
user.passwordChangedOn = now;
}
if (emailAddress) {
user.email = { isVerified: false, address: emailAddress };
}
// Ensure email address unicity
const emailAddress: string | undefined = payload.emailAddress?.trim();
if (emailAddress && users.findByEmail(emailAddress)) {
return;
}

// Save the user
users.value.set(id, user);
usernameIndex.value.set(usernameNormalized, id);
if (emailAddressNormalized) {
emailIndex.value.set(emailAddressNormalized, id);
}
if (payload.password) {
passwordHashes.value.set(id, hash(payload.password));
}
// Prepare user information
const firstName: string | undefined = payload.firstName?.trim();
const lastName: string | undefined = payload.lastName?.trim();
const fullName: string | undefined = buildFullName(firstName, lastName);

// Generate an unique identifier and a timestamp
const id: string = nanoid();
const now: string = new Date().toISOString();

// Create an actor
const actor: Actor = {
id,
type: "User",
isDeleted: false,
displayName: fullName ?? username,
emailAddress,
};

// Create the user
const user: User = {
id,
version: 1,
createdBy: actor,
createdOn: now,
updatedBy: actor,
updatedOn: now,
username,
hasPassword: Boolean(payload.password),
isDisabled: false,
isConfirmed: false,
firstName,
lastName,
fullName,
};
if (payload.password) {
user.passwordChangedBy = actor;
user.passwordChangedOn = now;
}
if (emailAddress) {
user.email = { isVerified: false, address: emailAddress };
}

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);
// Save the user
const passwordHash: string | undefined = payload.password ? hash(payload.password) : undefined;
users.save(user, passwordHash);
}

// Try finding the user by email address
if (!id) {
id = emailIndex.value.get(usernameNormalized);
}
function signIn(payload: SignInPayload): Actor {
// Initialize storage
const users = new UserRepository(localStorage);

// 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
}
// Try finding the user by username or email address
const user: User | undefined = users.findByUsername(payload.username) ?? users.findByEmail(payload.username);

// Checking credentials
if (!user || user.isDisabled) {
throw { code: "InvalidCredentials" } as ErrorDetail;
}
if (user.hasPassword) {
const password: string | undefined = users.findPassword(user);
if (!password || !payload.password || hash(payload.password) !== password) {
throw { code: "InvalidCredentials" } as ErrorDetail;
}
}

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

return toActor(user);
}
return toActor(user);
}

function verifyEmail(emailAddress: string): Actor | undefined {
// Find the user identifier using the email address
const emailAddressNormalized: string = emailAddress.trim().toUpperCase();
const id: string | undefined = emailIndex.value.get(emailAddressNormalized);
if (!id) {
return;
}
function verifyEmail(emailAddress: string): Actor | undefined {
// Initialize storage
const users = new UserRepository(localStorage);

// Find the user
const user: User | undefined = users.value.get(id);
if (!user?.email) {
return;
}
// Find the user by the email address
const user: User | undefined = users.findByEmail(emailAddress);
if (!user?.email) {
return;
}

// Check the user email is not already verified
if (user.email.isVerified) {
return;
}
// Check the user email is not already verified
if (user.email.isVerified) {
return;
}

// Create an actor from the user
const actor: Actor = toActor(user);
// Create an actor from the user
const actor: Actor = toActor(user);

// Verify the user email
user.email.isVerified = true;
user.email.verifiedBy = actor;
user.email.verifiedOn = new Date().toISOString();
// Verify the user email
user.email.isVerified = true;
user.email.verifiedBy = actor;
user.email.verifiedOn = new Date().toISOString();

// Confirm the user
user.isConfirmed = true;
// Confirm the user
user.isConfirmed = true;
users.save(user);

return actor;
}
return actor;
}

return { create, signIn, verifyEmail };
},
{ persist: true }, // TODO(fpion): does not seem to work because Map<K,V> is not serializable
);
return { create, signIn, verifyEmail };
});
64 changes: 64 additions & 0 deletions src/stores/userRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { User } from "@/types/users";

function getEmailKey(address: string): string {
return `user:email:${address.trim().toLowerCase()}`;
}
function getIdKey(id: string): string {
return `user:id:${id.trim()}`;
}
function getPasswordKey(user: User): string {
return `user:id:${user.id.trim()}:password`;
}
function getUsernameKey(username: string): string {
return `user:username:${username.trim().toLowerCase()}`;
}

class UserRepository {
storage: Storage;

constructor(storage: Storage) {
this.storage = storage;
}

find(id: string): User | undefined {
const serialized: string | null = this.storage.getItem(getIdKey(id));
if (!serialized) {
return;
}
return JSON.parse(serialized) as User;
}

findByEmail(address: string): User | undefined {
const id: string | null = this.storage.getItem(getEmailKey(address));
if (!id) {
return;
}
return this.find(id);
}

findByUsername(username: string): User | undefined {
const id: string | null = this.storage.getItem(getUsernameKey(username));
if (!id) {
return;
}
return this.find(id);
}

findPassword(user: User): string | undefined {
return this.storage.getItem(getPasswordKey(user)) ?? undefined;
}

save(user: User, passwordHash?: string): void {
this.storage.setItem(getIdKey(user.id), JSON.stringify(user));
this.storage.setItem(getUsernameKey(user.username), user.id);
if (user.email) {
this.storage.setItem(getEmailKey(user.email?.address), user.id);
}

if (passwordHash) {
this.storage.setItem(getPasswordKey(user), passwordHash);
}
}
}

export default UserRepository;
4 changes: 4 additions & 0 deletions src/types/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type ErrorDetail = {
code: string;
message?: string;
};
12 changes: 4 additions & 8 deletions src/views/account/SignInView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import type { Actor } from "@/types/aggregate";
import type { ErrorDetail } from "@/types/api";
import type { SignInPayload } from "@/types/account";
import { signIn } from "@/api/account";
import { useAccountStore } from "@/stores/account";
Expand All @@ -25,8 +26,9 @@ async function submit(): Promise<void> {
account.signIn(actor);
const redirect = route.query.redirect as string | undefined;
router.push(redirect ?? { name: "Profile" });
} catch (e) {
if (e === "INVALID_CREDENTIALS") {
} catch (e: unknown) {
const { code } = e as ErrorDetail;
if (code === "InvalidCredentials") {
invalidCredentials.value = true;
} else {
console.error(e); // TODO(fpion): error handling
Expand All @@ -36,12 +38,6 @@ async function submit(): Promise<void> {
}
}
}
import { useUserStore } from "@/stores/user";
const users = useUserStore();
users.create({
username: "fpion",
});
</script>

<template>
Expand Down

0 comments on commit cdafdab

Please sign in to comment.