Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Editor configuration, see https://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false

[*.md]
max_line_length = off
trim_trailing_whitespace = false
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: CI

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

jobs:
build:
name: Lint, test and build
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Check formatting
run: npx prettier --check "src/**/*.{ts,html,css}"

- name: Unit tests
run: npx ng test --watch=false

- name: Production build
run: npx ng build --configuration production
44 changes: 44 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.

# Compiled output
/dist
/tmp
/out-tsc
/bazel-out

# Node
/node_modules
npm-debug.log
yarn-error.log

# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/mcp.json
.history/*

# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
__screenshots__/

# System files
.DS_Store
Thumbs.db
5 changes: 5 additions & 0 deletions .postcssrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}
12 changes: 12 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"printWidth": 100,
"singleQuote": true,
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
}
]
}
4 changes: 4 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}
20 changes: 20 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}
42 changes: 42 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "Changes detected"
},
"endsPattern": {
"regexp": "bundle generation (complete|failed)"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "Changes detected"
},
"endsPattern": {
"regexp": "bundle generation (complete|failed)"
}
}
}
}
]
}
114 changes: 113 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,113 @@
# admin_frontend
# OpenTaberna Admin

Back-office for the OpenTaberna store: what customers can see, what is in stock,
what has been ordered and what is coming back.

Admin only. Every screen here is behind the `admin` realm role, and the API
additionally refuses admin endpoints unless the token was issued to this client
— so an administrator signed into the storefront cannot drive the back office
from there.

## Running it

The API and Keycloak must be up first (in the `fastapi` repository):

```bash
docker compose -f docker-compose.dev.yml up -d
```

Then:

```bash
npm install
npm start # http://localhost:4200
```

You will be redirected to Keycloak. In development:

| Username | Password | Result |
|---|---|---|
| `adminuser` | `adminpassword` | Full access |
| `testuser` | `testpassword` | "Not an administrator" screen |

## Architecture

The same structure is intended for the storefront, so both frontends read the
same way.

```
src/
app/
core/ # singletons — one instance, injected anywhere
auth/ # Keycloak session, HTTP interceptor, admin guard
api/ # one typed client per API domain
models/ # interfaces mirroring the API schemas
shared/ui/ # reusable presentational components. No API calls.
layout/ # the frame: shell, sidenav, topbar
features/ # one folder per screen, lazy-loaded
styles.css # THE global design file
environments/ # API and Keycloak URLs per build
```

Four rules keep it that way:

1. **`core/` is injected, never imported into a template.** Services and
guards live here; anything with a template does not.
2. **`shared/ui/` never talks to the API.** A component that fetches cannot be
reused on a screen that already has the data.
3. **`features/` never talks to `HttpClient` directly** — always through a
`core/api` client, so the base URL and error shape stay in one place.
4. **No design literals in templates.** No hex colour, no `rounded-[7px]`. If a
token does not exist, add it to `styles.css`.

### The global design file

`src/styles.css` is the single source of truth for how the product looks:
Tailwind v4 `@theme` tokens for colour, radius, shadow and type, then a small
set of semantic classes (`.card`, `.field-control`, `.data-table`) for patterns
repeated across many screens.

Semantic classes are deliberately few. Something used once stays a utility in
the template; a class earns its place only when copying the utility string
around would let screens drift apart.

Status colour is assigned by meaning rather than by feature, so `danger` means
the same thing on an order, a stock level and a return. A reader learns the
palette once.

### Reusable components

`shared/ui` exports `ot-button`, `ot-badge`, `ot-card`, `ot-modal`,
`ot-page-header`, `ot-empty-state`, `ot-spinner`, `ot-alert` and the `money`
pipe. They are standalone and imported per feature, so a lazy chunk pulls in
only what it renders.

`ot-empty-state` exists because "no data" and "still loading" otherwise look
identical, which is the most common way an admin screen confuses its operator.

### Authentication

`core/auth` owns the whole story. `AuthService` runs the authorization-code +
PKCE flow through `keycloak-js`, an `APP_INITIALIZER` completes it before the
first route resolves, and an interceptor attaches the token — but only to
requests aimed at `apiBaseUrl`, never to a third-party host.

`adminGuard` is a usability guard, not a security boundary. The API enforces
access on its own; the guard exists so a signed-in customer gets an explanation
instead of a screen of failing requests.

## Commands

```bash
npm start # dev server on :4200
npm run build # production bundle
npx ng test --watch=false # unit tests
npx prettier --check "src/**/*.{ts,html,css}"
```

## Configuration

`src/environments/environment.ts` holds the API base URL and the Keycloak
realm/client. The client id must stay `opentaberna-admin-ui`: the API checks the
token's `azp` against its list of admin clients, and any other client's token is
refused on `/v1/admin/**`.
73 changes: 73 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "npm"
},
"newProjectRoot": "projects",
"projects": {
"admin-frontend": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "admin-frontend:build:production"
},
"development": {
"buildTarget": "admin-frontend:build:development"
}
},
"defaultConfiguration": "development"
},
"test": {
"builder": "@angular/build:unit-test"
}
}
}
}
}
Loading
Loading