Template
Ports nextjs-templates/shopify-storefront to Vite, TanStack Router/Query, Tailwind v4 and Hono, without the AI store assistant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
169 lines
4.2 KiB
TypeScript
169 lines
4.2 KiB
TypeScript
// Classic Storefront customer accounts. The access token only ever lives in
|
|
// the httpOnly session cookie; none of these routes return it.
|
|
import { Hono } from 'hono';
|
|
import {
|
|
activateAccount,
|
|
createCustomer,
|
|
customerErrorMessage,
|
|
getCustomer,
|
|
login,
|
|
logout,
|
|
recoverPassword,
|
|
resetPassword,
|
|
toCustomerGid,
|
|
} from '@/services/shopify/customer';
|
|
import {
|
|
clearSessionToken,
|
|
getSessionToken,
|
|
setSessionToken,
|
|
} from './session';
|
|
|
|
type Body = Record<string, string | undefined>;
|
|
|
|
// A malformed body reads as empty, so each route's own validation answers it.
|
|
const readBody = (req: Request): Promise<Body> =>
|
|
req.json().catch(() => ({}));
|
|
|
|
export const account = new Hono();
|
|
|
|
// Minimal session probe for the header menu.
|
|
account.get('/me', async (c) => {
|
|
const token = getSessionToken(c);
|
|
if (!token) return c.json({ customer: null });
|
|
|
|
const customer = await getCustomer(token, 0);
|
|
if (!customer) return c.json({ customer: null });
|
|
|
|
return c.json({
|
|
customer: {
|
|
displayName: customer.displayName,
|
|
email: customer.email,
|
|
firstName: customer.firstName,
|
|
},
|
|
});
|
|
});
|
|
|
|
// Full profile with recent orders, for the order history page.
|
|
account.get('/customer', async (c) => {
|
|
const token = getSessionToken(c);
|
|
if (!token) return c.json({ customer: null });
|
|
|
|
const customer = await getCustomer(token, 20);
|
|
return c.json({ customer });
|
|
});
|
|
|
|
account.post('/login', async (c) => {
|
|
const { email, password } = await readBody(c.req.raw);
|
|
|
|
if (!email || !password) {
|
|
return c.json({ error: 'Enter your email and password.' }, 400);
|
|
}
|
|
|
|
const { token, errors } = await login(email, password);
|
|
|
|
if (!token) {
|
|
// Shopify distinguishes wrong-password from unknown-email; collapse both so
|
|
// the form can't be used to enumerate accounts.
|
|
return c.json(
|
|
{
|
|
error: errors.length
|
|
? 'Incorrect email or password.'
|
|
: customerErrorMessage(errors),
|
|
},
|
|
401
|
|
);
|
|
}
|
|
|
|
setSessionToken(c, token.accessToken, token.expiresAt);
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
account.post('/logout', async (c) => {
|
|
const token = getSessionToken(c);
|
|
if (token) await logout(token);
|
|
|
|
clearSessionToken(c);
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
account.post('/register', async (c) => {
|
|
const { email, password, firstName, lastName } = await readBody(c.req.raw);
|
|
|
|
if (!email || !password) {
|
|
return c.json({ error: 'Enter your email and password.' }, 400);
|
|
}
|
|
|
|
const { errors } = await createCustomer({
|
|
email,
|
|
password,
|
|
firstName,
|
|
lastName,
|
|
});
|
|
|
|
if (errors.length) {
|
|
return c.json({ error: customerErrorMessage(errors) }, 400);
|
|
}
|
|
|
|
// Sign the new customer straight in. Accounts needing email confirmation
|
|
// won't return a token yet, which is not an error.
|
|
const { token } = await login(email, password);
|
|
|
|
if (token) {
|
|
setSessionToken(c, token.accessToken, token.expiresAt);
|
|
return c.json({ ok: true, signedIn: true });
|
|
}
|
|
|
|
return c.json({ ok: true, signedIn: false });
|
|
});
|
|
|
|
account.post('/recover', async (c) => {
|
|
const { email } = await readBody(c.req.raw);
|
|
|
|
if (email) await recoverPassword(email);
|
|
|
|
// Always the same response, so the form can't reveal who has an account.
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
account.post('/reset', async (c) => {
|
|
const { id, resetToken, password } = await readBody(c.req.raw);
|
|
|
|
if (!id || !resetToken || !password) {
|
|
return c.json({ error: 'This reset link is incomplete.' }, 400);
|
|
}
|
|
|
|
const { token, errors } = await resetPassword(
|
|
toCustomerGid(id),
|
|
resetToken,
|
|
password
|
|
);
|
|
|
|
if (!token) {
|
|
return c.json({ error: customerErrorMessage(errors) }, 400);
|
|
}
|
|
|
|
setSessionToken(c, token.accessToken, token.expiresAt);
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
account.post('/activate', async (c) => {
|
|
const { id, activationToken, password } = await readBody(c.req.raw);
|
|
|
|
if (!id || !activationToken || !password) {
|
|
return c.json({ error: 'This activation link is incomplete.' }, 400);
|
|
}
|
|
|
|
const { token, errors } = await activateAccount(
|
|
toCustomerGid(id),
|
|
activationToken,
|
|
password
|
|
);
|
|
|
|
if (!token) {
|
|
return c.json({ error: customerErrorMessage(errors) }, 400);
|
|
}
|
|
|
|
setSessionToken(c, token.accessToken, token.expiresAt);
|
|
return c.json({ ok: true });
|
|
});
|