Kursproject — Utveckling av digitala tjänster · Högskolan Dalarna
A Swedish UNESCO World Heritage discovery platform delivered as an embeddable newspaper widget. Users find nearby heritage sites via geolocation, authenticate with email/password or Swedish BankID, subscribe for premium access, and unlock AI-powered chat, multi-language translation, and SMS/email notifications.
- About the Project
- Screenshots
- Features
- Tech Stack
- Architecture
- Team & Contributions
- Getting Started
- Environment Variables
- API Reference
- Testing
- Deployment
- License
Nordic Digital Solutions presents as a Swedish newspaper widget called DAGSTIDNINGEN. Embedded on any web page, it surfaces UNESCO World Heritage sites within 150 km of the user's location on an interactive map. Authenticated premium members can:
- Chat with an AI guide (Anthropic Claude) about any listed site
- Translate site descriptions into 100+ languages
- Receive SMS and email notifications when they are near a site they have not yet visited
Authentication supports both classic email/password with optional TOTP two-factor authentication, and Swedish BankID — the national e-ID used by millions of Swedes.
The project was built as a five-person university assignment demonstrating a realistic, service-oriented full-stack architecture with real third-party integrations.
To add screenshots: run the app locally (
python -m uvicorn app:app --reload), take screenshots of each view, and save them indocs/screenshots/with the filenames below. All placeholder paths will automatically become live images once the files exist.
The DAGSTIDNINGEN newspaper widget as it appears embedded on a web page. A mock Swedish newspaper article fills the page; the UNESCO "WORLD HERITAGE SERVICE" banner ad in the article body is the entry point to the entire service.
After clicking the banner ad, the visitor modal opens showing an interactive Leaflet.js map with markers for all UNESCO World Heritage sites within 150 km of the user's current location. The distance and site count are shown in the summary card above the map.
Below the map, a scrollable list of heritage sites sorted by distance from the user. Each row shows the site name, country, category (cultural/natural/mixed), and distance in kilometres. Clicking a row opens the site detail view.
The detail panel for a selected UNESCO site. Shows the full site description in English by default. The language dropdown (powered by Google Cloud Translate, 100+ languages) lets visitors instantly translate the description into Swedish, Arabic, Japanese, or any other supported language.
The premium sign-up form at the bottom of the visitor widget. Collects phone number (for SMS notifications), name, email, and password. The payment method selector offers "Faktura demo" (invoice mock) or "Kort" (Stripe card checkout). BankID is available as an alternative to password-based registration.
> *When the user clicks "Bekräfta med BankID", a choice panel appears: "BankID på den här enheten" opens the BankID app directly on the same device; "Mobilt BankID (QR-kod)" displays an animated QR code for scanning with a mobile device.*
The animated QR code panel for mobile BankID. The code refreshes every second (HMAC-SHA256 animation per the BankID specification). The user opens the BankID app on their phone and scans the code to authenticate. Status polling happens automatically every 2 seconds in the background.
The invoice billing form modal. Collects full name, street address, postcode, city, email for the invoice, and an optional reference/cost-centre field. The invoice mock provider stores the submission in memory and immediately activates the subscription.
The "Mina Sidor" (My Pages) modal for returning premium members. Supports email/password login and BankID login side by side. After a successful BankID login, the user is recognised by their Swedish personal number and the JWT token is issued automatically.
The TOTP challenge screen that appears after a correct email/password login when 2FA is enabled. The user enters the 6-digit code from their authenticator app (Google Authenticator, Authy, etc.) to complete sign-in.
The 2FA setup panel inside "Mina Sidor". Shows a QR code provisioning URI that the user scans with an authenticator app to link their account. After scanning, the user enters a code from the app to confirm and enable 2FA.
The logged-in premium member view. Shows the user's name, email, logout button, and the option to permanently delete the account and cancel the subscription. Below the user card, the nearest UNESCO site card appears with a "Jag har besökt denna plats" (I have visited this site) checkbox that suppresses future notifications for that site.
The full-size interactive Leaflet.js map available only to premium members. Compared to the visitor map, this view includes richer site data, member-specific overlays, and is tied to the user's stored home coordinates for personalised distance calculations.
The "AI-Guide (Premium)" chatbot panel powered by Anthropic Claude Haiku. Members can ask questions about any UNESCO World Heritage site shown on the map — history, significance, visiting tips, and more. The chatbot responds in the user's own language and politely declines questions unrelated to UNESCO heritage.
The inline confirmation prompt that appears when a member clicks "Avsluta prenumeration och radera konto" (Cancel subscription and delete account). Requires explicit confirmation before permanently deleting the user record and cancelling the subscription.
- Email and password registration and login
- Swedish BankID e-ID (device and mobile QR-code flows)
- Time-based One-Time Password (TOTP) two-factor authentication
- JWT access tokens with configurable expiry
- User profile management (name, home address, coordinates)
- Account deletion
- Geolocation-based UNESCO World Heritage site lookup
- Haversine distance filtering (default 150 km radius)
- Interactive map with site markers (Leaflet.js)
- 1-hour server-side site cache
- AI chatbot powered by Anthropic Claude Haiku
- Multi-language site translation (100+ languages via Google Cloud Translate)
- Premium member dashboard ("Mina Sidor")
- SMS notifications via HelloSMS REST API
- Email notifications via SMTP2GO REST API
- Location-based trigger (auto-send when user is near a subscribed site)
- Anti-spam cooldowns: 30 days (SMS), 7 days (email)
- Subscribe / unsubscribe per site
- Mark site as visited (suppresses future notifications)
- GDPR-compliant Swedish message templates
- Stripe card checkout (test mode)
- Invoice mock provider (in-memory)
- Subscription management (create, cancel, status)
- Mock mode for BankID, notifications, payments — no real credentials needed for local development
- SQLite fallback for tests; PostgreSQL for production
- GitHub Actions CI (Python 3.12, pytest)
- Heroku / Railway deployment via
Procfile
| Layer | Technology |
|---|---|
| Backend framework | FastAPI (Python 3.12) |
| ASGI server | Uvicorn |
| ORM | SQLAlchemy + Alembic migrations |
| Database | PostgreSQL (production), SQLite (tests) |
| Authentication | bcrypt, python-jose (JWT), pyotp (TOTP) |
| Frontend | Vanilla JavaScript, CSS Grid, Leaflet.js, qrcode.js |
| AI | Anthropic Claude Haiku (claude-haiku-4-5-20251001) |
| Translation | Google Cloud Translate v2 |
| Payments | Stripe (test mode) |
| SMS | HelloSMS REST API |
| SMTP2GO REST API | |
| E-ID | Swedish BankID (test environment) |
| CI/CD | GitHub Actions + Heroku/Railway |
┌─────────────────────────────────────────────────────────┐
│ Browser (Widget) │
│ index.html · style.css · script.js · Leaflet.js │
└──────────────────────────┬──────────────────────────────┘
│ HTTP / REST
┌──────────────────────────▼──────────────────────────────┐
│ FastAPI (app.py) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ ┌──────────┐ │
│ │ auth/ │ │ unesco/ │ │notification│ │translation│ │
│ │ (Sam) │ │ (Sonia) │ │ (Riyaaq) │ │ (Nina) │ │
│ └────┬─────┘ └────┬─────┘ └─────┬──────┘ └────┬─────┘ │
│ │ │ │ │ │
│ │ ┌───┴──────────────┴──────────────┘ │
│ ┌────┴─────┐ │ ┌──────────┐ │
│ │ payment/ │ │ PostgreSQL / SQLite │ core/ │ │
│ │ (Nina) │ │ (SQLAlchemy + Alembic) │ config │ │
│ └──────────┘ └─────────────────────────────┴──────────┘ │
└─────────────────────────────────────────────────────────┘
│ │ │ │
BankID UNESCO API HelloSMS / Stripe /
(e-ID) (open data) SMTP2GO Google
(notif.) Translate /
Anthropic
Each of the five services is a self-contained module with its own models.py, schemas.py, service.py, routes.py, and providers.py. The shared core/ package provides database sessions, Pydantic settings, and error helpers.
| # | Name | Service / Role | Key Responsibilities | Commits |
|---|---|---|---|---|
| 1 | Sam Sahbaei Razavi | services/auth/ · Backend lead |
Email/password auth, JWT tokens, Swedish BankID integration (device + mobile QR), TOTP two-factor auth, user model and profile management, FastAPI app wiring (app.py) |
77 |
| 2 | Riyaaq Ali | services/notification/ |
SMS/email notification engine, HelloSMS & SMTP2GO provider integrations, anti-spam cooldown logic, subscriber management, location-based notification triggers, PostgreSQL notification tables | 55 |
| 3 | Sonia Tolouifar | services/unesco/ |
UNESCO Open Data API integration, Haversine distance filtering, 1-hour site cache, Anthropic Claude Haiku AI chatbot for premium users | 31 |
| 4 | Nina Bentmosse | services/translation/ + services/payment/ |
Google Cloud Translate v2 (100+ languages, 3-tier OAuth fallback), Stripe card checkout, invoice mock provider, subscription management | 22 |
| 5 | Amanda Bejeryd | Frontend | Newspaper widget UI/UX, Swedish newspaper theme (DAGSTIDNINGEN), CSS Grid responsive layout, modal system, Leaflet.js map integration, JavaScript client-side logic | 10 |
- Python 3.12+
- PostgreSQL (or use SQLite for local testing)
- Git
# 1. Clone the repository
git clone https://github.com/Sam-Razavi/Nordic-Digital-Solutions.git
cd Nordic-Digital-Solutions
# 2. Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 3. Install dependencies
pip install -r requirements.txt
# 4. Set up environment variables
cp .env.example .env
# Edit .env with your values (see Environment Variables below)
# 5. Run database migrations
alembic upgrade head
# 6. Start the development server
python -m uvicorn app:app --reloadOpen http://localhost:8000 in your browser — the newspaper widget loads immediately.
Set these values in .env to run without any real external credentials:
BANKID_MOCK_MODE=true
NOTIFICATION_MOCK_MODE=true
NOTIFICATION_STORAGE_MOCK_MODE=true
# Leave STRIPE_SECRET_KEY, ANTHROPIC_API_KEY, GOOGLE_APPLICATION_CREDENTIALS emptyCopy .env.example to .env and fill in the values. Variables marked required must be set for that feature to work. All others fall back to mock/in-memory mode automatically.
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL connection string, e.g. postgresql://user:pass@localhost:5432/nordic |
| Variable | Required | Default | Description |
|---|---|---|---|
SECRET_KEY |
Yes | — | Long random string used to sign JWT tokens |
ALGORITHM |
No | HS256 |
JWT signing algorithm |
ACCESS_TOKEN_EXPIRE_MINUTES |
No | 60 |
JWT token lifetime in minutes |
| Variable | Required | Default | Description |
|---|---|---|---|
BANKID_MOCK_MODE |
No | false |
Set true to skip real BankID and auto-complete all logins |
BANKID_BASE_URL |
No | https://appapi2.test.bankid.com |
BankID API base URL |
BANKID_CERT_FILE |
Yes (real mode) | — | Path to .pem client certificate |
BANKID_CERT_PASSWORD |
Yes (real mode) | — | Certificate password |
BANKID_CA_FILE |
Yes (real mode) | — | Path to BankID CA certificate |
BANKID_END_USER_IP |
No | 127.0.0.1 |
IP passed to BankID API (use real user IP in production) |
Test certificates are available at bankid.com/en/utvecklare.
| Variable | Required | Description |
|---|---|---|
STRIPE_SECRET_KEY |
No | Stripe secret key. Must start with sk_test_. Leave empty to use invoice mock. |
STRIPE_PUBLISHABLE_KEY |
No | Stripe publishable key for client-side checkout |
PAYMENT_PROVIDER |
No | stripe or invoice (defaults to invoice if STRIPE_SECRET_KEY is missing) |
| Variable | Required | Description |
|---|---|---|
GOOGLE_APPLICATION_CREDENTIALS |
No | Path to service account JSON file |
GOOGLE_CLIENT_SECRET_FILE |
No | Path to OAuth client secret JSON (browser flow fallback) |
Leave both empty to use the built-in mock translation provider.
| Variable | Required | Description |
|---|---|---|
ANTHROPIC_API_KEY |
No | Anthropic API key for Claude Haiku. Leave empty to disable AI chat. |
| Variable | Required | Default | Description |
|---|---|---|---|
NOTIFICATION_MOCK_MODE |
No | true |
Set false to send real SMS/email |
HELLOSMS_API_URL |
No | https://api.hellosms.se/v1/ |
HelloSMS endpoint |
HELLOSMS_USERNAME |
No | — | HelloSMS account username |
HELLOSMS_PASSWORD |
No | — | HelloSMS account password |
SMTP2GO_API_URL |
No | https://api.smtp2go.com/v3/ |
SMTP2GO endpoint |
SMTP2GO_API_KEY |
No | — | SMTP2GO API key |
SMTP2GO_SENDER |
No | noreply@nordicdigitalsolutions.se |
From address for outbound emails |
NOTIFICATION_ADMIN_TOKEN |
No | changeme |
Bearer token for admin notification endpoints |
NOTIFICATION_COOLDOWN_SMS_HOURS |
No | 720 (30 days) |
Minimum hours between SMS to the same user about the same site |
NOTIFICATION_COOLDOWN_EMAIL_HOURS |
No | 168 (7 days) |
Minimum hours between email to the same user about the same site |
SEND_WELCOME_NOTIFICATIONS |
No | false |
Set true to send a real welcome message on new subscriptions |
NOTIFICATION_STORAGE_MOCK_MODE |
No | false |
Force in-memory notification storage (no DB required) |
| Variable | Required | Default | Description |
|---|---|---|---|
CORS_ORIGINS |
No | http://localhost:8000,http://127.0.0.1:8000 |
Comma-separated list of allowed origins. Use * for open access (disables credentials). |
All endpoints are served by the FastAPI app. Interactive documentation is available at /docs (Swagger UI) and /redoc when the server is running.
| Method | Path | Description |
|---|---|---|
GET |
/ |
Serves the newspaper widget (HTML) |
GET |
/widget |
Alias for the widget |
GET |
/health |
Health check — returns {"status": "ok"} |
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/auth/register |
— | Register with email and password |
POST |
/auth/login |
— | Login with email and password; returns JWT or 2FA temp token |
POST |
/auth/login/2fa |
temp token | Complete login with TOTP code |
GET |
/auth/me |
JWT | Get current user profile |
PATCH |
/auth/me/profile |
JWT | Update name, address, or coordinates |
DELETE |
/auth/account |
JWT | Delete account permanently |
POST |
/auth/subscription/activate |
JWT | Mark user as premium subscriber |
POST |
/auth/2fa/setup |
JWT | Generate TOTP secret and provisioning QR URI |
POST |
/auth/2fa/enable |
JWT | Enable 2FA by verifying a TOTP code |
POST |
/auth/2fa/disable |
JWT | Disable 2FA by verifying a TOTP code |
GET |
/auth/2fa/status |
JWT | Check whether 2FA is currently enabled |
POST |
/auth/bankid/initiate |
— | Start a BankID auth session; returns orderRef and initial QR data |
GET |
/auth/bankid/qr/{order_ref} |
— | Fetch the latest animated QR code data for polling |
GET |
/auth/bankid/status/{order_ref} |
— | Poll BankID completion status; returns JWT on success |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/unesco/sites |
— | List UNESCO World Heritage sites within radius_km of lat/lon |
POST |
/unesco/chat |
JWT (premium) | Send a message to the Claude AI chatbot about UNESCO sites |
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/notification/send |
Admin token | Manually send SMS or email to a subscriber |
GET |
/api/notification/trigger |
— | Trigger location-based notifications for a user position |
POST |
/api/notification/subscribe |
JWT | Subscribe to notifications for a specific site |
POST |
/api/notification/unsubscribe |
JWT | Unsubscribe from a specific site |
POST |
/api/notification/mark-visited |
JWT | Mark a site as visited (suppresses future notifications) |
GET |
/api/notification/admin/list |
Admin token | List all subscribers (admin only) |
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/translation/languages |
— | List all supported languages (ISO-639-1 codes + display names) |
POST |
/translation/translate |
— | Translate text into a target language |
POST |
/v3beta1/projects/{project_id}:translateText |
— | Google Cloud Translate v3beta1-compatible endpoint |
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/payment/create |
JWT | Create a Stripe checkout session or invoice subscription |
POST |
/payment/cancel |
JWT | Cancel the current subscription |
GET |
/payment/subscription/{id} |
— | Get subscription status by ID |
GET |
/payment/lyckades |
— | Stripe success redirect page (Swedish: "succeeded") |
GET |
/payment/avbruten |
— | Stripe cancellation redirect page (Swedish: "cancelled") |
Tests use pytest with a SQLite in-memory database and mock providers for BankID and notifications.
# Run all tests
pytest -q
# Run a specific service's tests
pytest services/auth/
pytest services/notification/
pytest services/translation/
pytest services/payment/GitHub Actions runs the full test suite on every push and pull request to main. See .github/workflows/ci.yml for the pipeline configuration.
The CI pipeline:
- Uses Python 3.12 on Ubuntu
- Sets
BANKID_MOCK_MODE=trueandNOTIFICATION_MOCK_MODE=true - Uses a SQLite test database (no PostgreSQL required)
The Procfile contains the production start command:
web: alembic upgrade head && uvicorn app:app --host 0.0.0.0 --port $PORT
This runs pending Alembic database migrations on every deploy before starting the server.
- Create a new app on Heroku or Railway
- Add a PostgreSQL add-on and copy the
DATABASE_URL - Set all required environment variables in the platform dashboard
- Push the repository — the
Procfilehandles the rest
DATABASE_URL # PostgreSQL connection string
SECRET_KEY # Long random string
BANKID_MOCK_MODE # true unless you have real BankID test certs
ANTHROPIC_API_KEY # Required for AI chat
STRIPE_SECRET_KEY # Required for card payments
GOOGLE_APPLICATION_CREDENTIALS # Required for translation
NOTIFICATION_MOCK_MODE # false for real SMS/email
A
docker-compose.ymlis not included in this repository. To containerise the app, you would need to provide a PostgreSQL service and mount a.envfile.
This project was created for educational purposes at Högskolan Dalarna as part of the course Utveckling av digitala tjänster.
Built with FastAPI, BankID, Stripe, Google Cloud Translate, Anthropic Claude, HelloSMS, and SMTP2GO.