Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a new type called UserSession and use it in the UserNav component #41

Merged
merged 1 commit into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
Loading