Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6ae1a2c
feat(shared): describe the admin sections and the roles that open them
Nascimento95 Oct 12, 2026
efb4cc5
feat(ui): add the shared web package with the shadcn theme read from …
Nascimento95 Oct 12, 2026
fa2a3d6
feat(admin): set up the Next.js app, never indexed and never cached
Nascimento95 Oct 12, 2026
495c2f9
feat(admin): describe the navigation of each role and the page to ret…
Nascimento95 Oct 12, 2026
e74ffad
feat(admin): hold the session of a team member, read from Firebase an…
Nascimento95 Oct 12, 2026
7bad428
feat(admin): sign in with Google or an e-mail link (A-01)
Nascimento95 Oct 12, 2026
c68d3e2
feat(admin): frame the pages with the navigation of the role and the …
Nascimento95 Oct 12, 2026
fc58756
feat(shared): add the admin counters and the mfa_required and account…
Nascimento95 Oct 12, 2026
af8fd4a
feat(api): add the journal of admin actions and the suspension of an …
Nascimento95 Oct 12, 2026
67201f9
feat(api): guard the admin routes by section and serve the navigation…
Nascimento95 Oct 12, 2026
3c53b50
feat(admin): show the counters in the navigation and refuse a suspend…
Nascimento95 Oct 12, 2026
48096ae
feat(ui): add the field controls, the filter chip and the status badge
Nascimento95 Oct 12, 2026
22ee626
feat(ui): add the table, the detail panel, the confirmation and the t…
Nascimento95 Oct 12, 2026
0610112
feat(admin): keep the state of a list in its address, format dates an…
Nascimento95 Oct 12, 2026
2667af7
chore(infra): add the Docker image of the admin for Cloud Run
Nascimento95 Oct 12, 2026
0b506c6
fix(admin): read the environment when the headers are asked for, not …
Nascimento95 Oct 12, 2026
098adfc
fix(admin): upgrade next to 16.3.8 and depend on Firebase Auth only
Nascimento95 Oct 12, 2026
2e97f8f
feat(admin): carry the second factor of a session through the identit…
Nascimento95 Oct 13, 2026
2d93ecc
feat(admin): set up and ask for the authenticator code at sign-in (D-…
Nascimento95 Oct 13, 2026
967c5a5
test(api): copy a named demo route far from the search zones in the a…
Nascimento95 Oct 13, 2026
2fd515d
fix(admin): sign in again when the API ends the session, and refuse a…
Nascimento95 Oct 14, 2026
3ff68e2
fix(api): take the Auth emulator with a demo project only, and the TO…
Nascimento95 Oct 14, 2026
a52d652
test(api): check that every admin route refuses a caller outside the …
Nascimento95 Oct 14, 2026
b181be3
chore: describe the react-dom that pnpm links next to the app
Nascimento95 Oct 14, 2026
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
**/dist
**/coverage
**/.turbo
**/.next
**/.env
**/.env.*
!**/.env.example
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ dist/
coverage/
.turbo/
*.tsbuildinfo
# Next.js build output, and the route types written by `next typegen`.
.next/
next-env.d.ts
*.log

.env
Expand Down
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ pnpm-lock.yaml
.expo/
apps/mobile/ios/
apps/mobile/android/
# Next.js build output (apps/admin).
.next/
19 changes: 19 additions & 0 deletions apps/admin/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Admin web (apps/admin). Copy to .env.local for development. The values are written in the pages
# at build time: staging and production each build their own image. Nothing here is a secret:
# every NEXT_PUBLIC_ value is sent to the browser.

# Origin of the API, without path or trailing slash. The API must list the origin of the admin in
# its CORS_ORIGINS (http://localhost:5173 locally).
NEXT_PUBLIC_API_URL=http://localhost:8080

# Web configuration of the Firebase project (Firebase console › Project settings › Your apps).
# These identify the project and are not secrets; staging and production each have their own.
# The values below only exist in the local Auth emulator.
NEXT_PUBLIC_FIREBASE_API_KEY=demo-key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=demo-widoo.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=demo-widoo

# Local Auth emulator (`firebase emulators:start --only auth --project demo-widoo`), the same as
# FIREBASE_AUTH_EMULATOR_HOST of the API. Accepted with a demo-* project only; empty on staging
# and production.
NEXT_PUBLIC_FIREBASE_AUTH_EMULATOR_URL=http://127.0.0.1:9099
36 changes: 36 additions & 0 deletions apps/admin/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Admin web — règles de travail

Outil interne de l'équipe Widoo. Spécification : wiki [Admin-Web](https://github.com/Inprogress-Agency/widoo-app/wiki/Admin-Web) ; écrans de référence : [Ecrans › Admin web](https://github.com/Inprogress-Agency/widoo-app/wiki/Ecrans#admin-web). Les règles communes du dépôt sont dans le `CLAUDE.md` de la racine.

## Architecture des fichiers

| Dossier | Rôle |
|---|---|
| `src/app/` | routes seulement : pages fines ; `(shell)/` regroupe les pages derrière la connexion, `connexion/` est A-01 |
| `src/containers/` | assemblage d'un écran : lit la session et les données par les hooks, passe des props aux composants |
| `src/components/` | composants d'affichage, sans appel réseau ni règle métier : `shell/`, `sign-in/`, `states/` |
| `src/hooks/` | logique d'écran (`useSignIn`, `useSession`, `useMediaQuery`) |
| `src/lib/` | session et fournisseur d'identité (`auth/`), page de retour, en-têtes de réponse |
| `src/config/` | navigation par rôle, variables d'environnement validées |
| `src/messages/` | textes, en français seulement |

Les composants communs (bouton, champ, encadré, menu, info-bulle) et le thème shadcn tiré de `tokens.json` sont dans `packages/ui`.

## Règles

- **Aucune donnée en rendu serveur.** Les pages sont des coques statiques ; tout appel à l'API part du navigateur, par `packages/api-client`, avec le jeton de la personne connectée.
- **L'API décide.** Le rôle est vérifié par l'API à chaque appel ; l'admin ne fait que cacher ce que le rôle ne permet pas (`packages/shared`, `admin-sections.ts`).
- **Aucun texte en dur.** Chaque texte visible vient de `src/messages/fr.ts`.
- **Aucune couleur ni valeur de style en dur.** Classes du preset `@widoo/ui/tailwind-preset` seulement ; pas de valeur arbitraire (`text-[15px]`). Une taille des planches absente de `tokens.json` s'ajoute au preset avec la mention « à reporter dans tokens.json ».
- **Aucune valeur d'environnement en dur.** `src/config/env.ts`, documentées dans `.env.example` ; rien de secret : tout `NEXT_PUBLIC_` part dans le navigateur.
- **Jamais indexé, jamais mis en cache** : en-têtes de `src/lib/response-headers.ts`.
- **Imports** par l'alias `@/`.

## Commandes

```bash
cp apps/admin/.env.example apps/admin/.env.local
pnpm --filter @widoo/admin dev # http://localhost:5173
pnpm --filter @widoo/admin build && pnpm --filter @widoo/admin start
pnpm --filter @widoo/admin lint && pnpm --filter @widoo/admin typecheck && pnpm --filter @widoo/admin test
```
36 changes: 36 additions & 0 deletions apps/admin/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { NextConfig } from 'next';
import { parseEnv } from './src/config/env';
import { responseHeaders } from './src/lib/response-headers';

const dev = process.env.NODE_ENV === 'development';

const nextConfig: NextConfig = {
// Self-contained server for the Docker image of Cloud Run.
output: 'standalone',
// The monorepo root, so that the standalone output traces the workspace packages.
outputFileTracingRoot: new URL('../../', import.meta.url).pathname,
// Workspace packages shipped as TypeScript sources.
transpilePackages: ['@widoo/ui', '@widoo/shared', '@widoo/api-client'],
poweredByHeader: false,
reactStrictMode: true,
// apps/admin/AGENTS.md holds the rules of the admin: `next dev` must not append its own block.
agentRules: false,
// The environment is read here, when the server or the build asks for the headers, and not
// when this file loads: `next typegen` (typecheck, CI) needs no environment.
headers: async () => {
const env = parseEnv(process.env);
return [
{
source: '/:path*',
headers: responseHeaders({
dev,
apiOrigin: env.NEXT_PUBLIC_API_URL,
authDomain: env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
authEmulatorOrigin: env.NEXT_PUBLIC_FIREBASE_AUTH_EMULATOR_URL,
}),
},
];
},
};

export default nextConfig;
28 changes: 27 additions & 1 deletion apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,33 @@
"private": true,
"type": "module",
"scripts": {
"dev": "next dev --port 5173",
"build": "next build",
"start": "next start --port 5173",
"lint": "eslint .",
"typecheck": "tsc"
"typecheck": "next typegen && tsc",
"test": "vitest run"
},
"dependencies": {
"@firebase/app": "^0.16.3",
"@firebase/auth": "^1.13.7",
"@phosphor-icons/react": "^2.1.10",
"@widoo/api-client": "workspace:*",
"@widoo/shared": "workspace:*",
"@widoo/ui": "workspace:*",
"next": "16.3.8",
"qrcode": "^1.5.4",
"react": "19.2.3",
"react-dom": "19.2.3",
"zod": "^4.6.5"
},
"devDependencies": {
"@types/node": "^22.20.4",
"@types/qrcode": "^1.5.6",
"@types/react": "~19.2.2",
"@types/react-dom": "~19.2.2",
"autoprefixer": "^10.6.1",
"postcss": "^8.5.28",
"tailwindcss": "3.4.19"
}
}
3 changes: 3 additions & 0 deletions apps/admin/postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
plugins: { tailwindcss: {}, autoprefixer: {} },
};
1 change: 1 addition & 0 deletions apps/admin/public/brand/google-g.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions apps/admin/public/brand/widoo-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions apps/admin/src/app/(shell)/[...rest]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { notFound } from 'next/navigation';

/** Any other address under the shell: the missing page, shown inside the shell. */
export default function UnknownPage() {
notFound();
}
6 changes: 6 additions & 0 deletions apps/admin/src/app/(shell)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ShellContainer } from '@/containers/ShellContainer';
import type { ReactNode } from 'react';

export default function ShellLayout({ children }: { children: ReactNode }) {
return <ShellContainer>{children}</ShellContainer>;
}
19 changes: 19 additions & 0 deletions apps/admin/src/app/(shell)/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { PageMessage } from '@/components/states/PageMessage';
import { sectionPaths } from '@/config/navigation';
import { messages } from '@/messages/fr';
import { Button } from '@widoo/ui';
import Link from 'next/link';

export default function NotFound() {
return (
<PageMessage
title={messages.notFound.title}
text={messages.notFound.text}
action={
<Button asChild>
<Link href={sectionPaths.dashboard}>{messages.notFound.back}</Link>
</Button>
}
/>
);
}
7 changes: 7 additions & 0 deletions apps/admin/src/app/(shell)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* A-05, the dashboard: its tiles and figures come with the ticket of the dashboard (#55). The
* shell around it already shows its title.
*/
export default function DashboardPage() {
return null;
}
11 changes: 11 additions & 0 deletions apps/admin/src/app/connexion/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { SignInContainer } from '@/containers/SignInContainer';
import { Suspense } from 'react';

/** A-01. The page asked for is read from the address, in the browser only. */
export default function SignInPage() {
return (
<Suspense>
<SignInContainer />
</Suspense>
);
}
3 changes: 3 additions & 0 deletions apps/admin/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
27 changes: 27 additions & 0 deletions apps/admin/src/app/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions apps/admin/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import './globals.css';
import { SessionProvider } from '@/lib/auth/SessionProvider';
import { messages } from '@/messages/fr';
import { Toaster } from '@widoo/ui';
import type { Metadata } from 'next';
import { Plus_Jakarta_Sans } from 'next/font/google';
import type { ReactNode } from 'react';

// Self-hosted at build time: no request to Google from the browser.
const jakarta = Plus_Jakarta_Sans({
subsets: ['latin'],
// The weights of the board « Typographie » (wiki Ecrans › Admin web).
weight: ['400', '500', '600', '700', '800'],
display: 'swap',
variable: '--font-jakarta',
});

export const metadata: Metadata = {
title: messages.app.title,
robots: { index: false, follow: false },
};

export default function RootLayout({ children }: { children: ReactNode }) {
return (
// Browser extensions write attributes on <html> before React loads: accepted on this element
// only, its children are still checked.
<html lang="fr" className={jakarta.variable} suppressHydrationWarning>
<body className="bg-background font-sans text-body-s text-foreground antialiased">
<SessionProvider>{children}</SessionProvider>
<Toaster />
</body>
</html>
);
}
6 changes: 6 additions & 0 deletions apps/admin/src/app/robots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { MetadataRoute } from 'next';

/** No page of the admin is public: closed to every crawler, on top of the `noindex` header. */
export default function robots(): MetadataRoute.Robots {
return { rules: { userAgent: '*', disallow: '/' } };
}
60 changes: 60 additions & 0 deletions apps/admin/src/components/confirm/ReasonFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { messages } from '@/messages/fr';
import { Field, Select, Textarea } from '@widoo/ui';

export type ReasonOption = { code: string; label: string };

type ReasonFieldsProps = {
/** Prefix of the `id` of the two controls, unique on the page. */
id: string;
/** The codified reasons of the action, in the order of the wiki. */
options: readonly ReasonOption[];
reasonCode: string;
onReasonCodeChange: (code: string) => void;
detail: string;
onDetailChange: (detail: string) => void;
};

/**
* The reason of a destructive action: a codified reason and its detail, both required (wiki
* Admin-Web › Modèles communs). Goes inside a `ConfirmDialog`, whose form checks them.
*/
export function ReasonFields({
id,
options,
reasonCode,
onReasonCodeChange,
detail,
onDetailChange,
}: ReasonFieldsProps) {
const texts = messages.confirm;
return (
<>
<Field label={texts.reason} htmlFor={`${id}-reason`} isRequired>
<Select
id={`${id}-reason`}
required
value={reasonCode}
onChange={(event) => onReasonCodeChange(event.target.value)}
>
<option value="" disabled>
{texts.chooseReason}
</option>
{options.map((option) => (
<option key={option.code} value={option.code}>
{option.label}
</option>
))}
</Select>
</Field>
<Field label={texts.detail} htmlFor={`${id}-detail`} isRequired hint={texts.detailHint}>
<Textarea
id={`${id}-detail`}
required
aria-describedby={`${id}-detail-hint`}
value={detail}
onChange={(event) => onDetailChange(event.target.value)}
/>
</Field>
</>
);
}
Loading
Loading