Skip to content

Commit

Permalink
Merge pull request #41 from Jim-Hodapp-Coaching/dynamic_user_nav_menu
Browse files Browse the repository at this point in the history
Add a new type called UserSession and use it in the UserNav component
  • Loading branch information
jhodapp authored Oct 10, 2024
2 parents 1fa724a + 090564a commit 9321f56
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 108 deletions.
22 changes: 16 additions & 6 deletions src/components/ui/user-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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>
Expand All @@ -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}>
Expand Down
24 changes: 13 additions & 11 deletions src/components/user-auth-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement> {}

export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
const router = useRouter();
const { userId } = useAuthStore((state) => state);
const { login } = useAuthStore((action) => action);

const [isLoading, setIsLoading] = React.useState<boolean>(false);
Expand All @@ -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) => {
Expand Down
127 changes: 70 additions & 57 deletions src/lib/api/user-session.ts
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 "";
};
69 changes: 35 additions & 34 deletions src/lib/stores/auth-store.ts
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;
};
66 changes: 66 additions & 0 deletions src/types/user-session.ts
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);
}

0 comments on commit 9321f56

Please sign in to comment.