Skip to content
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ pnpm-debug.log*

# misc
.DS_Store

.junie
15 changes: 15 additions & 0 deletions src/auth/authClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ export const fetchCurrentUser = async (): Promise<SessionUser> => {
return data;
};

// === Update role (server is the source of truth) ===
export const saveRole = async (
role: "retailer" | "producer" | "enthusiast"
): Promise<SessionUser> => {
const token = storage.getToken();
// Backend expects RoleEnum, which is typically uppercase. Send uppercase to avoid 400.
const body = { role: role.toUpperCase() } as unknown as { role: string };
const {data} = await api.patch(
"/session/user",
body,
{headers: {Authorization: `Bearer ${token}`, "Content-Type": "application/json"}}
);
return data;
};

export const startAuthorization = (userId: string): void => {
if (!userId) {
console.warn("Cannot start Square OAuth: missing userId");
Expand Down
37 changes: 35 additions & 2 deletions src/auth/authMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export const authMachine = setup({
| { type: "LOGGED_IN"; data: SessionUser }
| { type: "LOGGED_OUT" }
| { type: "POS.LOAD"; provider: PosProvider; merchantId: string }
| { type: "POS.REFRESH"; provider: PosProvider; merchantId: string },
| { type: "POS.REFRESH"; provider: PosProvider; merchantId: string }
| { type: "ROLE.SET"; role: import('./types').Role },
},
actors: {

Expand Down Expand Up @@ -70,6 +71,15 @@ export const authMachine = setup({

if (payload) {
const user = payload as SessionUser;
try {
const prevRole = context.user?.user?.role?.value ?? "visitor";
const nextRole = user?.user?.role?.value ?? "visitor";
if (prevRole !== nextRole) {
console.info("[role] machine saveUser role transition", { from: prevRole, to: nextRole, reason: e?.type ?? "unknown" });
}
} catch (err) {
console.warn("[role] saveUser transition logging failed", err);
}
storage.setUser(user);
return user;
}
Expand All @@ -93,6 +103,26 @@ export const authMachine = setup({
}),
}),

// Client-only: set role in local user and persist
setRole: assign({
user: ({context, event}) => {
const e = event as { type: string; role?: import('./types').Role };
if (!context.user || !e.role) return context.user;
const next = {
...context.user,
user: {
...context.user.user,
role: {
...context.user.user.role,
value: e.role,
},
},
} as SessionUser;
storage.setUser(next);
return next;
},
}),

setPosLoading: assign({
pos: ({context}) => ({
...context.pos,
Expand Down Expand Up @@ -211,7 +241,10 @@ export const authMachine = setup({
},
LOGGED_IN: {
actions: "saveUser"
}
},
"ROLE.SET": {
actions: "setRole",
},
},
// Nested state for authenticated
states: {
Expand Down
2 changes: 1 addition & 1 deletion src/auth/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ type UserRole = {
export type SessionUser = {
user: User;
token: string;
permissions: string[];
};

export type PosToken = {
Expand All @@ -40,6 +39,7 @@ export type AuthContextValue = {
login: (data: SessionUser) => void;
logout: () => void;
fetchUser: () => Promise<SessionUser | null>;
updateRole: (role: Role) => void;
role: Role;
isVisitor: boolean;
isRetailer: boolean;
Expand Down
35 changes: 34 additions & 1 deletion src/auth/useAuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {useSelector} from "@xstate/react";
import type {ActorRefFrom} from "xstate";
import {authMachine} from "./authMachine";
import type {Role, SessionUser} from "./types";
import {fetchCurrentUser} from "./authClient.ts";
import {fetchCurrentUser, saveRole} from "./authClient.ts";

type AuthService = ActorRefFrom<typeof authMachine>;

Expand Down Expand Up @@ -51,6 +51,39 @@ export const useAuthService = (service: AuthService) => {
refreshPos: async (provider: "square" | "clover", merchantId: string): Promise<void> => {
send({type: "POS.REFRESH", provider, merchantId});
},
updateRole: (nextRole: Role) => {
// Disallow setting non-selectable roles from UI
if (nextRole === "visitor" || nextRole === "admin") {
console.warn("[role] blocked attempt to set disallowed role", { nextRole });
return;
}
// Persist to backend and rehydrate session user
(async () => {
try {
console.info("[role] updating role →", { from: role, to: nextRole });
const updated = await saveRole(nextRole as any);
const newRole = (updated?.user?.role?.value ?? "visitor") as Role;
console.info("[role] role updated ✔", { from: role, to: newRole, userId: updated?.user?.id });
send({type: "LOGGED_IN", data: updated});

// After a successful role change, redirect to role-specific profile when applicable
const normalized = String(newRole).toLowerCase() as Role;
if (normalized === "retailer") {
const retailerId = updated?.user?.role?.id;
if (retailerId) {
const targetPath = `/retailer/${retailerId}/profile`;
if (window.location.pathname !== targetPath) {
window.location.assign(targetPath);
}
} else {
console.warn("[role] retailer selected but missing role.id; staying on current page");
}
}
} catch (e) {
console.error("[role] update failed ✖", e);
}
})();
},
role,
isVisitor: role === "visitor",
isRetailer: role === "retailer",
Expand Down
32 changes: 28 additions & 4 deletions src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@ import Hero from "../components/common/Hero.tsx";
* @param type
*/
export const HomePage: React.FC<{ userType: string }> = (type) => {
if (type.userType === "visitor") {
return <VisitorHomePage />;
} else if (type.userType === "retailer") {
return <RetailerHomePage />;
switch ((type.userType || "visitor").toLowerCase()) {
case "retailer":
return <RetailerHomePage />;
case "producer":
return <ProducerHomePage />;
case "enthusiast":
return <EnthusiastHomePage />;
case "visitor":
default:
return <VisitorHomePage />;
}
};

Expand All @@ -30,3 +36,21 @@ const RetailerHomePage = () => {
/>
);
};

const ProducerHomePage = () => {
return (
<Hero
subHeading="Showcase Your Craft"
desc="Set up your producer profile, present your portfolio, and connect with retailers and enthusiasts discovering your wines."
/>
);
};

const EnthusiastHomePage = () => {
return (
<Hero
subHeading="Explore, Learn, Enjoy"
desc="Browse wineries and retailers, save favorites, and deepen your wine knowledge — read-only for now."
/>
);
};
Loading