update ui update and fix layout issue

This commit is contained in:
2026-08-06 13:21:10 +05:30
parent 92322bff16
commit e5f8144fb3
251 changed files with 9041 additions and 2119 deletions

View 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'}});
}

View 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'}},
);
}

View 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'}});
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;