update ui update and fix layout issue
This commit is contained in:
117
src/app/api/auth/login/route.ts
Normal file
117
src/app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import {cookies} from 'next/headers';
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {verifyCredentials} from '@/features/auth/mock/users.mock';
|
||||
import {
|
||||
REMEMBERED_MAX_AGE_SECONDS,
|
||||
SESSION_COOKIE,
|
||||
SESSION_MAX_AGE_SECONDS,
|
||||
createSessionToken,
|
||||
sessionCookieOptions,
|
||||
} from '@/features/auth/services/sessionToken';
|
||||
import type {AuthSession, LoginError} from '@/features/auth/types/auth';
|
||||
import type {ApiFailure, ApiSuccess} from '@/shared/types/api';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/**
|
||||
* POST /api/auth/login
|
||||
*
|
||||
* The seam a real backend replaces. Everything above it — the repository, the
|
||||
* service, the form — speaks in {credentials} → {session | error} and does not
|
||||
* care whether the check happened against a fixture or an identity provider.
|
||||
*
|
||||
* Responsibilities that deliberately live HERE and not in the client:
|
||||
* • deciding whether the credentials are valid
|
||||
* • deciding how long the session lasts (rememberMe is a request, not an
|
||||
* instruction — the server sets the cookie lifetime)
|
||||
* • issuing the httpOnly cookie the client can never read or forge
|
||||
*/
|
||||
|
||||
interface LoginRequestBody {
|
||||
email?: unknown;
|
||||
password?: unknown;
|
||||
rememberMe?: unknown;
|
||||
}
|
||||
|
||||
function failure(error: LoginError, status: number): Response {
|
||||
// Shaped as the app's standard envelope so the client's error path is the
|
||||
// same one every other endpoint uses; `field` rides alongside for the form.
|
||||
const body: ApiFailure & {field: LoginError['field']} = {
|
||||
error: {code: status === 401 ? 'unauthorized' : 'bad_request', message: error.message},
|
||||
field: error.field,
|
||||
};
|
||||
return Response.json(body, {status, headers: {'cache-control': 'no-store'}});
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest): Promise<Response> {
|
||||
let body: LoginRequestBody;
|
||||
try {
|
||||
body = (await req.json()) as LoginRequestBody;
|
||||
} catch {
|
||||
return failure({field: 'form', message: 'Malformed request body.'}, 400);
|
||||
}
|
||||
|
||||
const email = typeof body.email === 'string' ? body.email.trim() : '';
|
||||
const password = typeof body.password === 'string' ? body.password : '';
|
||||
const rememberMe = body.rememberMe === true;
|
||||
|
||||
// Server-side validation, repeated rather than trusted from the client. The
|
||||
// form validates too, for latency; this validates because the form is not a
|
||||
// security boundary and a POST can arrive without it.
|
||||
if (!email) {
|
||||
return failure({field: 'email', message: 'Enter your email address.'}, 400);
|
||||
}
|
||||
if (!/^\S+@\S+\.\S+$/.test(email)) {
|
||||
return failure(
|
||||
{field: 'email', message: 'Enter a valid email address.'},
|
||||
400,
|
||||
);
|
||||
}
|
||||
if (!password) {
|
||||
return failure({field: 'password', message: 'Enter your password.'}, 400);
|
||||
}
|
||||
|
||||
const check = verifyCredentials(email, password);
|
||||
|
||||
if (check.outcome === 'unknown_email' || check.outcome === 'wrong_password') {
|
||||
return failure(
|
||||
{field: 'form', message: 'Invalid email or password.'},
|
||||
401,
|
||||
);
|
||||
}
|
||||
|
||||
const maxAge = rememberMe
|
||||
? REMEMBERED_MAX_AGE_SECONDS
|
||||
: SESSION_MAX_AGE_SECONDS;
|
||||
|
||||
const token = createSessionToken(
|
||||
{
|
||||
sub: check.user.id,
|
||||
email: check.user.email,
|
||||
name: check.user.name,
|
||||
role: check.user.role,
|
||||
organisation: check.user.organisation,
|
||||
},
|
||||
maxAge,
|
||||
);
|
||||
|
||||
// A browser-session cookie still needs a server-side expiry, or a tab left
|
||||
// open for a week would hold a valid token indefinitely.
|
||||
const store = await cookies();
|
||||
store.set(
|
||||
SESSION_COOKIE,
|
||||
token,
|
||||
sessionCookieOptions(rememberMe ? maxAge : undefined),
|
||||
);
|
||||
|
||||
const session: AuthSession = {
|
||||
user: check.user,
|
||||
expiresAt: new Date(Date.now() + maxAge * 1000).toISOString(),
|
||||
};
|
||||
|
||||
const payload: ApiSuccess<AuthSession> = {
|
||||
data: session,
|
||||
meta: {generatedAt: new Date().toISOString()},
|
||||
};
|
||||
return Response.json(payload, {headers: {'cache-control': 'no-store'}});
|
||||
}
|
||||
29
src/app/api/auth/logout/route.ts
Normal file
29
src/app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {cookies} from 'next/headers';
|
||||
import {
|
||||
SESSION_COOKIE,
|
||||
sessionCookieOptions,
|
||||
} from '@/features/auth/services/sessionToken';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/**
|
||||
* POST /api/auth/logout
|
||||
*
|
||||
* Ending a session is a server action, not a client one: only the server can
|
||||
* invalidate an httpOnly cookie. The client clearing its own state would leave
|
||||
* the credential intact and the next request still authenticated.
|
||||
*
|
||||
* Overwritten with an expired value rather than only `.delete()`-ed — some
|
||||
* proxies drop a bare deletion, and an empty value fails signature
|
||||
* verification anyway, so the session is dead by two independent routes.
|
||||
*/
|
||||
export async function POST(): Promise<Response> {
|
||||
const store = await cookies();
|
||||
store.set(SESSION_COOKIE, '', sessionCookieOptions(0));
|
||||
store.delete(SESSION_COOKIE);
|
||||
|
||||
return Response.json(
|
||||
{data: {ok: true}, meta: {generatedAt: new Date().toISOString()}},
|
||||
{headers: {'cache-control': 'no-store'}},
|
||||
);
|
||||
}
|
||||
42
src/app/api/auth/session/route.ts
Normal file
42
src/app/api/auth/session/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {cookies} from 'next/headers';
|
||||
import {findUserById} from '@/features/auth/mock/users.mock';
|
||||
import {
|
||||
SESSION_COOKIE,
|
||||
verifySessionToken,
|
||||
} from '@/features/auth/services/sessionToken';
|
||||
import type {AuthSession} from '@/features/auth/types/auth';
|
||||
import type {ApiSuccess} from '@/shared/types/api';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/**
|
||||
* GET /api/auth/session
|
||||
*
|
||||
* The client's only source of truth about who it is. It cannot read the
|
||||
* httpOnly cookie, so it asks — and the answer comes from verifying a
|
||||
* signature, not from believing something the browser stored.
|
||||
*
|
||||
* Returns 200 with `data: null` for "no session" rather than 401. A signed-out
|
||||
* visitor is a normal state for this endpoint, not an error, and modelling it
|
||||
* as one means every caller has to special-case a failure that is not one.
|
||||
*
|
||||
* The user is re-resolved from the directory rather than read straight off the
|
||||
* token: a role change or a deactivation must take effect on the next request,
|
||||
* not whenever the cookie happens to expire.
|
||||
*/
|
||||
export async function GET(): Promise<Response> {
|
||||
const store = await cookies();
|
||||
const payload = verifySessionToken(store.get(SESSION_COOKIE)?.value);
|
||||
|
||||
const user = payload ? findUserById(payload.sub) : null;
|
||||
|
||||
const body: ApiSuccess<AuthSession | null> = {
|
||||
data:
|
||||
payload && user
|
||||
? {user, expiresAt: new Date(payload.exp * 1000).toISOString()}
|
||||
: null,
|
||||
meta: {generatedAt: new Date().toISOString()},
|
||||
};
|
||||
|
||||
return Response.json(body, {headers: {'cache-control': 'no-store'}});
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {ok, parseQuery, simulate, wantsEmpty} from '@/lib/api/server';
|
||||
import {buildActivity} from '@/lib/mock/dashboard';
|
||||
import {ok, parseQuery, requireApiSession, simulate, wantsEmpty} from '@/shared/services/apiRoute';
|
||||
import {buildActivity} from '@/features/dashboard/mock/dashboard.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {ok, parseQuery, simulate, wantsEmpty} from '@/lib/api/server';
|
||||
import {buildBriefing} from '@/lib/mock/briefing';
|
||||
import {ok, parseQuery, requireApiSession, simulate, wantsEmpty} from '@/shared/services/apiRoute';
|
||||
import {buildBriefing} from '@/features/dashboard/mock/briefing.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const EMPTY = {summary: '', alerts: [], tasks: []};
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {ok, parseQuery, simulate, wantsEmpty} from '@/lib/api/server';
|
||||
import {buildKpis} from '@/lib/mock/dashboard';
|
||||
import {ok, parseQuery, requireApiSession, simulate, wantsEmpty} from '@/shared/services/apiRoute';
|
||||
import {buildKpis} from '@/features/dashboard/mock/dashboard.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {ok, parseQuery, simulate, wantsEmpty} from '@/lib/api/server';
|
||||
import {buildPeakHours} from '@/lib/mock/dashboard';
|
||||
import {ok, parseQuery, requireApiSession, simulate, wantsEmpty} from '@/shared/services/apiRoute';
|
||||
import {buildPeakHours} from '@/features/dashboard/mock/dashboard.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import type {Granularity} from '@/lib/api/contracts';
|
||||
import {ok, parseQuery, simulate, wantsEmpty} from '@/lib/api/server';
|
||||
import {buildPeriodPerformance} from '@/lib/mock/analytics';
|
||||
import type {Granularity} from '@/features/dashboard/types/dashboard';
|
||||
import {ok, parseQuery, requireApiSession, simulate, wantsEmpty} from '@/shared/services/apiRoute';
|
||||
import {buildPeriodPerformance} from '@/features/dashboard/mock/analytics.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {ok, parseQuery, simulate, wantsEmpty} from '@/lib/api/server';
|
||||
import {buildRewardUsage} from '@/lib/mock/analytics';
|
||||
import {ok, parseQuery, requireApiSession, simulate, wantsEmpty} from '@/shared/services/apiRoute';
|
||||
import {buildRewardUsage} from '@/features/dashboard/mock/analytics.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {ok, parseQuery, simulate, wantsEmpty} from '@/lib/api/server';
|
||||
import {buildStoreComparison} from '@/lib/mock/analytics';
|
||||
import {ok, parseQuery, requireApiSession, simulate, wantsEmpty} from '@/shared/services/apiRoute';
|
||||
import {buildStoreComparison} from '@/features/dashboard/mock/analytics.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {ok, parseQuery, simulate, wantsEmpty} from '@/lib/api/server';
|
||||
import {buildTimeseries} from '@/lib/mock/dashboard';
|
||||
import {ok, parseQuery, requireApiSession, simulate, wantsEmpty} from '@/shared/services/apiRoute';
|
||||
import {buildTimeseries} from '@/features/dashboard/mock/dashboard.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {ok, parseQuery, simulate, wantsEmpty} from '@/lib/api/server';
|
||||
import {buildLytActivity} from '@/lib/mock/lyts';
|
||||
import {ok, parseQuery, requireApiSession, simulate, wantsEmpty} from '@/shared/services/apiRoute';
|
||||
import {buildLytActivity} from '@/features/lyts/mock/lyts.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {ok, parseQuery, simulate, wantsEmpty} from '@/lib/api/server';
|
||||
import {buildRedemptions} from '@/lib/mock/lyts';
|
||||
import {ok, parseQuery, requireApiSession, simulate, wantsEmpty} from '@/shared/services/apiRoute';
|
||||
import {buildRedemptions} from '@/features/lyts/mock/lyts.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {ok, parseQuery, simulate, wantsEmpty} from '@/lib/api/server';
|
||||
import {buildRewards} from '@/lib/mock/lyts';
|
||||
import {ok, parseQuery, requireApiSession, simulate, wantsEmpty} from '@/shared/services/apiRoute';
|
||||
import {buildRewards} from '@/features/lyts/mock/lyts.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {ok, parseQuery, simulate} from '@/lib/api/server';
|
||||
import {readProfile} from '@/lib/mock/settings';
|
||||
import {ok, parseQuery, requireApiSession, simulate} from '@/shared/services/apiRoute';
|
||||
import {readProfile} from '@/features/settings/mock/settings.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
@@ -19,6 +21,8 @@ export async function GET(req: NextRequest) {
|
||||
* which is enough for the form to exercise its success path honestly.
|
||||
*/
|
||||
export async function PATCH(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {ok, parseQuery, simulate, wantsEmpty} from '@/lib/api/server';
|
||||
import {buildAttendance} from '@/lib/mock/staff';
|
||||
import {ok, parseQuery, requireApiSession, simulate, wantsEmpty} from '@/shared/services/apiRoute';
|
||||
import {buildAttendance} from '@/features/staff/mock/staff.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {ok, parseQuery, simulate, wantsEmpty} from '@/lib/api/server';
|
||||
import {buildStaff} from '@/lib/mock/staff';
|
||||
import {ok, parseQuery, requireApiSession, simulate, wantsEmpty} from '@/shared/services/apiRoute';
|
||||
import {buildStaff} from '@/features/staff/mock/staff.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {ok, parseQuery, simulate} from '@/lib/api/server';
|
||||
import {buildStaffSummary} from '@/lib/mock/staff';
|
||||
import {ok, parseQuery, requireApiSession, simulate} from '@/shared/services/apiRoute';
|
||||
import {buildStaffSummary} from '@/features/staff/mock/staff.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {fail, ok, parseQuery, simulate} from '@/lib/api/server';
|
||||
import {buildStore} from '@/lib/mock/stores';
|
||||
import {fail, ok, parseQuery, requireApiSession, simulate} from '@/shared/services/apiRoute';
|
||||
import {buildStore} from '@/features/stores/mock/stores.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
@@ -8,6 +8,8 @@ export async function GET(
|
||||
req: NextRequest,
|
||||
{params}: {params: Promise<{storeId: string}>},
|
||||
) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type {NextRequest} from 'next/server';
|
||||
import {ok, parseQuery, simulate, wantsEmpty} from '@/lib/api/server';
|
||||
import {buildStores} from '@/lib/mock/stores';
|
||||
import {ok, parseQuery, requireApiSession, simulate, wantsEmpty} from '@/shared/services/apiRoute';
|
||||
import {buildStores} from '@/features/stores/mock/stores.mock';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const denied = await requireApiSession();
|
||||
if (denied) return denied;
|
||||
const q = parseQuery(req);
|
||||
const simulated = await simulate(q);
|
||||
if (simulated) return simulated;
|
||||
|
||||
Reference in New Issue
Block a user