// 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; // A malformed body reads as empty, so each route's own validation answers it. const readBody = (req: Request): Promise => 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 }); });