-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #41 from Jim-Hodapp-Coaching/dynamic_user_nav_menu
Add a new type called UserSession and use it in the UserNav component
- Loading branch information
Showing
5 changed files
with
200 additions
and
108 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,13 +16,18 @@ 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() { | ||
const router = useRouter(); | ||
|
||
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() { | |
<Button variant="ghost" className="relative mx-2 h-8 w-8 rounded-full"> | ||
<Avatar className="h-9 w-9"> | ||
<AvatarImage src="/avatars/03.png" alt="@jhodapp" /> | ||
<AvatarFallback>JH</AvatarFallback> | ||
<AvatarFallback> | ||
{userFirstLastLettersToString( | ||
userSession.first_name, | ||
userSession.last_name | ||
)} | ||
</AvatarFallback> | ||
</Avatar> | ||
</Button> | ||
</DropdownMenuTrigger> | ||
<DropdownMenuContent className="w-56" align="end" forceMount> | ||
<DropdownMenuLabel className="font-normal"> | ||
<div className="flex flex-col space-y-1"> | ||
<p className="text-sm font-medium leading-none">Jim Hodapp</p> | ||
<p className="text-sm font-medium leading-none">{`${userSession.first_name} ${userSession.last_name}`}</p> | ||
<p className="text-xs leading-none text-muted-foreground"> | ||
[email protected] | ||
{userSession.email} | ||
</p> | ||
</div> | ||
</DropdownMenuLabel> | ||
|
@@ -65,19 +75,19 @@ export function UserNav() { | |
Profile | ||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut> | ||
</DropdownMenuItem> | ||
<DropdownMenuItem> | ||
{/* <DropdownMenuItem> | ||
Current Organization | ||
<DropdownMenuShortcut>⌘O</DropdownMenuShortcut> | ||
</DropdownMenuItem> | ||
<DropdownMenuItem> | ||
Billing | ||
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut> | ||
</DropdownMenuItem> | ||
</DropdownMenuItem> */} | ||
<DropdownMenuItem> | ||
Settings | ||
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut> | ||
</DropdownMenuItem> | ||
<DropdownMenuItem>New Team</DropdownMenuItem> | ||
{/* <DropdownMenuItem>New Team</DropdownMenuItem> */} | ||
</DropdownMenuGroup> | ||
<DropdownMenuSeparator /> | ||
<DropdownMenuItem onClick={logout_user}> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<UserSession> => { | ||
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<string> => { | ||
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 ""; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<AuthStore>()( | ||
devtools( | ||
persist( | ||
(set) => ({ | ||
... initState, | ||
export const createAuthStore = (initState: AuthState = defaultInitState) => { | ||
const authStore = create<AuthStore>()( | ||
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string, unknown>; | ||
|
||
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); | ||
} |