From 090564a800eb1f7cf8a3a942c0708a2de079b734 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Thu, 10 Oct 2024 11:30:09 -0500 Subject: [PATCH] Add a new type called UserSession and use it in the UserNav component --- src/components/ui/user-nav.tsx | 22 ++++-- src/components/user-auth-form.tsx | 24 +++--- src/lib/api/user-session.ts | 127 ++++++++++++++++-------------- src/lib/stores/auth-store.ts | 69 ++++++++-------- src/types/user-session.ts | 66 ++++++++++++++++ 5 files changed, 200 insertions(+), 108 deletions(-) create mode 100644 src/types/user-session.ts diff --git a/src/components/ui/user-nav.tsx b/src/components/ui/user-nav.tsx index 7be0a2e..bf42dfa 100644 --- a/src/components/ui/user-nav.tsx +++ b/src/components/ui/user-nav.tsx @@ -16,6 +16,7 @@ import { import { logoutUser } from "@/lib/api/user-session"; import { useAppStateStore } from "@/lib/providers/app-state-store-provider"; import { useAuthStore } from "@/lib/providers/auth-store-provider"; +import { userFirstLastLettersToString } from "@/types/user-session"; import { useRouter } from "next/navigation"; export function UserNav() { @@ -23,6 +24,10 @@ export function UserNav() { const { logout } = useAuthStore((action) => action); + const { userSession } = useAuthStore((state) => ({ + userSession: state.userSession, + })); + const { reset } = useAppStateStore((action) => action); async function logout_user() { @@ -46,16 +51,21 @@ export function UserNav() {
-

Jim Hodapp

+

{`${userSession.first_name} ${userSession.last_name}`}

- jim@refactorcoach.com + {userSession.email}

@@ -65,19 +75,19 @@ export function UserNav() { Profile ⇧⌘P - + {/* Current Organization ⌘O Billing ⌘B - + */} Settings ⌘S - New Team + {/* New Team */} diff --git a/src/components/user-auth-form.tsx b/src/components/user-auth-form.tsx index 8a53466..e2c9ac9 100644 --- a/src/components/user-auth-form.tsx +++ b/src/components/user-auth-form.tsx @@ -10,12 +10,12 @@ import { Icons } from "@/components/ui/icons"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { userSessionToString } from "@/types/user-session"; interface UserAuthFormProps extends React.HTMLAttributes {} export function UserAuthForm({ className, ...props }: UserAuthFormProps) { const router = useRouter(); - const { userId } = useAuthStore((state) => state); const { login } = useAuthStore((action) => action); const [isLoading, setIsLoading] = React.useState(false); @@ -27,16 +27,18 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) { event.preventDefault(); setIsLoading(true); - const [userId, err] = await loginUser(email, password); - if (userId.length > 0 && err.length == 0) { - login(userId); - router.push("/dashboard"); - } else { - console.error("err: " + err); - setError(err); - } - - setIsLoading(false); + await loginUser(email, password) + .then((userSession) => { + console.debug("userSession: " + userSessionToString(userSession)); + login(userSession.id, userSession); + setIsLoading(false); + router.push("/dashboard"); + }) + .catch((err) => { + setIsLoading(false); + console.error("Login failed, err: " + err); + setError(err); + }); } const updateEmail = (value: string) => { diff --git a/src/lib/api/user-session.ts b/src/lib/api/user-session.ts index 2fdde2b..e425d29 100644 --- a/src/lib/api/user-session.ts +++ b/src/lib/api/user-session.ts @@ -1,69 +1,82 @@ // Interacts with the user_session_controller endpoints -import { Id } from "@/types/general"; +import { + defaultUserSession, + isUserSession, + parseUserSession, + UserSession, +} from "@/types/user-session"; import { AxiosError, AxiosResponse } from "axios"; -export const loginUser = async (email: string, password: string): Promise<[Id, string]> => { - const axios = require("axios"); +export const loginUser = async ( + email: string, + password: string +): Promise => { + const axios = require("axios"); - console.log("email: ", email); - console.log("password: ", password.replace(/./g, "*")); + console.log("email: ", email); + console.log("password: ", password.replace(/./g, "*")); - var userId: Id = ""; - var err: string = ""; + var userSession: UserSession = defaultUserSession(); + var err: string = ""; - const data = await axios - .post( - "http://localhost:4000/login", - { - email: email, - password: password, + const data = await axios + .post( + "http://localhost:4000/login", + { + email: email, + password: password, + }, + { + withCredentials: true, + headers: { + "Content-Type": "application/x-www-form-urlencoded", }, - { - withCredentials: true, - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - setTimeout: 5000, // 5 seconds before timing out trying to log in with the backend - } - ) - .then(function (response: AxiosResponse) { - // handle success - userId = response.data.data.id; - }) - .catch(function (error: AxiosError) { - // handle error - console.error(error.response?.status); - if (error.response?.status == 401) { - err = "Login failed: unauthorized"; - } else { - console.error(error); - err = `Login failed: ${error.message}`; - } - }) + setTimeout: 5000, // 5 seconds before timing out trying to log in with the backend + } + ) + .then(function (response: AxiosResponse) { + // handle success + const userSessionData = response.data.data; + if (isUserSession(userSessionData)) { + userSession = parseUserSession(userSessionData); + } + }) + .catch(function (error: AxiosError) { + // handle error + console.error(error.response?.status); + if (error.response?.status == 401) { + err = "Login failed: unauthorized"; + } else { + err = `Login failed: ${error.message}`; + } + }); - return [userId, err]; -} + if (err) { + console.error(err); + throw err; + } + + return userSession; +}; export const logoutUser = async (): Promise => { - const axios = require("axios"); + const axios = require("axios"); - const data = await axios - .get( - "http://localhost:4000/logout", - { - withCredentials: true, - setTimeout: 5000, // 5 seconds before timing out trying to log in with the backend - } - ) - .then(function (response: AxiosResponse) { - // handle success - console.debug(response); - }) - .catch(function (error: AxiosError) { - // handle error - console.error(`Logout failed: ${error.message}`); - return(`Logout failed: ${error.message}`); - }) + const data = await axios + .get("http://localhost:4000/logout", { + withCredentials: true, + setTimeout: 5000, // 5 seconds before timing out trying to log in with the backend + }) + .then(function (response: AxiosResponse) { + // handle success + console.debug(response); + }) + .catch(function (error: AxiosError) { + // handle error + const err = `Logout failed: ${error.message}`; + console.error(err); + return err; + }); - return ""; -} + return ""; +}; diff --git a/src/lib/stores/auth-store.ts b/src/lib/stores/auth-store.ts index 35895f5..3c656d6 100644 --- a/src/lib/stores/auth-store.ts +++ b/src/lib/stores/auth-store.ts @@ -1,47 +1,48 @@ -import { Id } from '@/types/general'; -import { create, useStore } from 'zustand'; -import { createJSONStorage, devtools, persist } from 'zustand/middleware'; +import { Id } from "@/types/general"; +import { defaultUserSession, UserSession } from "@/types/user-session"; +import { create, useStore } from "zustand"; +import { createJSONStorage, devtools, persist } from "zustand/middleware"; interface AuthState { - // Holds user id UUID from the backend DB schema for a User - userId: Id; - isLoggedIn: boolean; + // Holds user id UUID from the backend DB schema for a User + userId: Id; + userSession: UserSession; + isLoggedIn: boolean; } interface AuthActions { - login: (userId: Id) => void; - logout: () => void; + login: (userId: Id, userSession: UserSession) => void; + logout: () => void; } export type AuthStore = AuthState & AuthActions; export const defaultInitState: AuthState = { - userId: "", - isLoggedIn: false, -} + userId: "", + userSession: defaultUserSession(), + isLoggedIn: false, +}; -export const createAuthStore = ( - initState: AuthState = defaultInitState, -) => { - const authStore = create()( - devtools( - persist( - (set) => ({ - ... initState, +export const createAuthStore = (initState: AuthState = defaultInitState) => { + const authStore = create()( + devtools( + persist( + (set) => ({ + ...initState, - login: (userId) => { - set({ isLoggedIn: true, userId }); - }, - logout: () => { - set({ isLoggedIn: false, userId: undefined }); - }, - }), - { - name: 'auth-store', - storage: createJSONStorage(() => sessionStorage), - } - ) - ) + login: (userId, userSession) => { + set({ isLoggedIn: true, userId, userSession }); + }, + logout: () => { + set(defaultInitState); + }, + }), + { + name: "auth-store", + storage: createJSONStorage(() => sessionStorage), + } + ) ) - return authStore; -} + ); + return authStore; +}; diff --git a/src/types/user-session.ts b/src/types/user-session.ts new file mode 100644 index 0000000..e091eda --- /dev/null +++ b/src/types/user-session.ts @@ -0,0 +1,66 @@ +import { Id } from "@/types/general"; + +// This must always reflect the Rust struct on the backend +// controller::user_session_controller::login()'s return value +export interface UserSession { + id: Id; + email: string; + first_name: string; + last_name: string; + display_name: string; +} + +export function parseUserSession(data: any): UserSession { + if (!isUserSession(data)) { + throw new Error("Invalid UserSession object data"); + } + return { + id: data.id, + email: data.email, + first_name: data.first_name, + last_name: data.last_name, + display_name: data.display_name, + }; +} + +export function isUserSession(value: unknown): value is UserSession { + if (!value || typeof value !== "object") { + return false; + } + const object = value as Record; + + return ( + typeof object.id === "string" && + typeof object.email === "string" && + typeof object.first_name === "string" && + typeof object.last_name === "string" && + typeof object.display_name === "string" + ); +} + +export function defaultUserSession(): UserSession { + return { + id: "", + email: "", + first_name: "", + last_name: "", + display_name: "", + }; +} + +// Given first and last name strings, return the first letters of each as a new string +// e.g. "John" "Smith" => "JS" +export function userFirstLastLettersToString( + firstName: string, + lastName: string +): string { + const firstLetter = firstName.charAt(0); + const lastLetter = lastName.charAt(0); + return firstLetter + lastLetter; +} + +export function userSessionToString( + userSession: UserSession | undefined +): string { + return JSON.stringify(userSession); +}