Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# PostgreSQL init script must use LF as line endings
# Run `git ls-files --eol` to view applied attributes
configuration/os2iot-postgresql/initdb/*.sh text eol=lf
configuration/postgres/initdb/*.sh text eol=lf
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
.idea/
.env
ca.srl
ca.crt
ca.key
server.crt
server.key
server.csr
server.csr
92 changes: 92 additions & 0 deletions Development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Development

> [!WARNING]
> This document is still work in progress.

Start backend and frontend in development mode (cf. [`docker-compose.development.yml`](docker-compose.development.yml)):

``` shell
task dev:start
task setup:chirpstack
```

Nginx now exposes the frontend on <http://127.0.0.1:8888/> and the backend (API) on <http://127.0.0.1:8888/api/>, e.g.
<http://127.0.0.1:8888/api/v1/docs>.

> [!NOTE]
> You may have to wait quite a while until everything is up and running. And you may have to reload in your browser a
> couple of times …

Check the status development services:

``` shell
task dev:status
curl http://0.0.0.0:8888
curl http://0.0.0.0:8888/api/v1/docs
```

## Frontend

> [!WARNING]
> Incomplete section ahead!

Built with Angular.

## Backend

> [!WARNING]
> Incomplete section ahead!

Built with [Nest (NestJS)](https://docs.nestjs.com/).

``` shell
task dev:log SERVICE=backend
```

### Add new entity

1. Create a new class in `src/entities`, e.g.

``` node
// src/entities/contact-person.entity.ts
import { DbBaseEntity } from "./base.entity";

@Entity("contact_person")
export class ContactPerson extends DbBaseEntity {
// …
}
```

2. Add the entity to `TypeOrmModule.forFeature` in `src/modules/shared.module.ts`:

``` node
// src/modules/shared.module.ts

@Module({
imports: [
TypeOrmModule.forFeature([
// …
ContactPerson,
]),
// …
```

3. Generate a migration:

``` shell
docker compose --project-name os2iot-docker exec os2iot-backend npm run generate-migration src/migration/name-of-migration
```

### Add entity property (in API)

``` shell

```

---

Talk to the database:

``` shell
docker compose --project-name os2iot-docker exec os2iot-postgresql psql os2iot os2iot
```
61 changes: 32 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ task open
- Email: `global-admin@os2iot.dk`
- Password: `hunter2`

#### ChirpStack Integration

> [!NOTE]
> The backend must be able to talk to ChirpStack to create new applications etc.

Run

```bash
task setup:chirpstack
```

to generate and set an API key in `.env`, or do it manually:

1. Open ChirpStack UI: `task open:chirpstack`
2. Login with `admin` / `admin`
3. Go to **API Keys** → Create a new API key (`/#/api-keys/create`)
4. Enter key name and press Submit.
4. Add to `.env` file: `CHIRPSTACK_API_KEY=your-key-here`
5. Restart the backend: `docker compose up --detach os2iot-backend`

**Available tasks:**

```bash
Expand All @@ -71,23 +91,19 @@ task clean # Remove containers, volumes, and images
task open # Open frontend in browser
```

### ChirpStack Integration (Optional)
## Backend API

If you're using LoRaWAN features, you need to configure the ChirpStack API key:
Run

```bash
# Run the setup task - it will guide you through the process
task setup:chirpstack
``` shell
task backend:api-key:create
```

Or manually:
1. Open ChirpStack UI: `task open:chirpstack`
2. Login with `admin` / `admin`
3. Go to **API Keys** → Create a new API key
4. Add to `.env` file: `CHIRPSTACK_API_KEY=your-key-here`
5. Restart backend: `docker compose up -d os2iot-backend`
to generate a backend API key. Use to fetch data:

Without this configuration, you'll see `InvalidToken` errors in the backend logs - these can be ignored if you're not using LoRaWAN features.
``` shell
curl --header 'X-API-KEY: …' "http://$(docker compose port nginx 80)/api/v1/application"
```

## Configuration

Expand All @@ -98,6 +114,10 @@ Edit the files in the configuration folder to adjust settings for each requireme
- Postgres from the official image.
- Chirpstack using their Docker Compose

## Development

See [Development](./Development.md) for some details on how to start the containers in development mode.

## Troubleshooting FAQ

### Docker File Sharing issues
Expand All @@ -116,23 +136,6 @@ Docker doesn't have access to mount the volumes.
Solution:
On Windows: Go to Docker Desktop (tray icon) -> Settings -> Resources -> File Sharing -> Add the directory which is the parent directory of "OS2IoT-docker" or a parent of that. -> Apply & Restart

### error: database "os2iot-e2e" does not exist

```
[ExceptionHandler] Unable
to connect to the database. Retrying (1)...
error: database "os2iot-e2e" does not exist
at Parser.parseErrorMessage
```

Cause:
Database has not been setup correctly on local machine.

Solution:
docker compose down --volumes
dos2unix configuration/os2iot-postgresql/initdb/* # Run from git bash on Windows
docker compose up

### error: Error: connect ETIMEDOUT xxx.xxx.xxx.xxx:xxxx at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1141:16)

Cause:
Expand Down
120 changes: 82 additions & 38 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ tasks:
desc: Run all setup steps (clone repos, fix line endings, generate certs)
cmds:
- task: setup:clone-repos
- task: setup:fix-line-endings
- task: setup:certs
- task: setup:check

Expand All @@ -40,20 +39,6 @@ tasks:
echo "OS2IoT-frontend already exists"
fi

setup:fix-line-endings:
desc: Fix line endings on database init scripts (for Windows/cross-platform)
silent: true
cmds:
- |
if command -v dos2unix &> /dev/null; then
dos2unix configuration/os2iot-postgresql/initdb/*.sh 2>/dev/null || true
dos2unix configuration/postgres/initdb/*.sh 2>/dev/null || true
dos2unix configuration/postgresql/initdb/*.sh 2>/dev/null || true
echo "Line endings fixed"
else
echo "dos2unix not installed - skipping (install with: apt install dos2unix)"
fi

setup:certs:
desc: Generate MQTT broker certificates (CA and server)
silent: true
Expand Down Expand Up @@ -111,7 +96,7 @@ tasks:
fi
TOKEN=$(curl -s -X POST "http://localhost:${NGINX_PORT}/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{"username":"global-admin@os2iot.dk","password":"hunter2"}' | jq -r '.accessToken')
-d '{"username":"global-admin@os2iot.dk","password":"hunter2"}' | docker run -i --rm ghcr.io/jqlang/jq -r '.accessToken')
if [ "$TOKEN" = "null" ] || [ -z "$TOKEN" ]; then
echo "✗ Failed to login - ensure services are running"
exit 1
Expand All @@ -131,28 +116,7 @@ tasks:
silent: true
cmds:
- |
CHIRPSTACK_PORT=$(docker compose port chirpstack 8080 2>/dev/null | cut -d: -f2)
if [ -z "$CHIRPSTACK_PORT" ]; then
echo "✗ ChirpStack not running (run: docker compose up --detach)"
exit 1
fi
echo "=== ChirpStack API Key Setup ==="
echo ""
echo "1. Opening ChirpStack UI at http://localhost:${CHIRPSTACK_PORT}"
echo "2. Login with: admin / admin"
echo "3. Go to: API Keys (in the left menu)"
echo "4. Click 'Add API key'"
echo "5. Give it a name and click 'Submit'"
echo "6. Copy the generated token"
echo ""
# Try to open in browser
if command -v xdg-open &> /dev/null; then
xdg-open "http://localhost:${CHIRPSTACK_PORT}" 2>/dev/null &
elif command -v open &> /dev/null; then
open "http://localhost:${CHIRPSTACK_PORT}" 2>/dev/null &
fi
echo "Paste your API key here (or press Ctrl+C to cancel):"
read -r API_KEY
API_KEY=$(docker compose exec chirpstack chirpstack --config /etc/chirpstack/ create-api-key --name test-development | grep 'token: ' | sed 's/^[^:]*: *//')
if [ -n "$API_KEY" ]; then
if [ -f ".env" ]; then
if grep -q "^CHIRPSTACK_API_KEY=" .env; then
Expand Down Expand Up @@ -214,3 +178,83 @@ tasks:
else
echo "Open in browser: $URL"
fi

backend:api-key:create:
desc: Create backend API key
silent: true
cmds:
- |
NGINX_PORT=$(docker compose port nginx 80 | cut -d: -f2)
if [ -z "$NGINX_PORT" ]; then
echo "✗ Services not running (run: docker compose up --detach)"
exit 1
fi
TOKEN=$(curl -s -X POST "http://localhost:${NGINX_PORT}/api/v1/auth/login" \
-H "Content-Type: application/json" \
-d '{"username":"global-admin@os2iot.dk","password":"hunter2"}' | docker run -i --rm ghcr.io/jqlang/jq -r '.accessToken')
if [ "$TOKEN" = "null" ] || [ -z "$TOKEN" ]; then
echo "✗ Failed to login - ensure services are running"
exit 1
fi

# Permissions
#
# id | name
# ----+---------------------------------------------------
# 2 | Default Organization - Læserettigheder
# 3 | Default Organization - Applikationsadministrator
# 4 | Default Organization - Organisationsadministrator

RESULT=$(curl -s -X POST "http://localhost:${NGINX_PORT}/api/v1/api-key" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"test-development", "expiresOn": "2100-01-01T00:00:00Z", "permissionIds": [ 2, 3, 4 ]}')
if echo "$RESULT" | grep -q '"key"'; then
API_KEY=$(echo "$RESULT" | docker run -i --rm ghcr.io/jqlang/jq --raw-output '.key')
echo "✓ API key:"
echo
echo "$API_KEY"
else
echo "Error creating API key: $RESULT"
fi

dev:start:
desc: Start containers in development mode
cmds:
- task: dev:compose
vars:
TASK_ARGS: up --build --detach --wait

dev:stop:
desc: Stop containers in development mode
cmds:
- task: dev:compose
vars:
TASK_ARGS: stop

dev:status:
desc: Show status of development containers
cmds:
# We use `printf` to escape having Task processing the placeholders.
- docker compose --file docker-compose.development.yml ps os2iot-frontend os2iot-backend --format '{{printf "{{.Image}} {{.State}}\t{{.Status}}"}}'

dev:log:
desc: "Follow development container log; examples: `task {{.TASK}} SERVICE=frontend` or `task {{.TASK}} --interactive`"
# https://taskfile.dev/blog/if-and-variable-prompt#prompt-for-required-variables
requires:
vars:
- name: SERVICE
enum: [frontend, backend]
cmds:
- task: dev:compose
vars:
TASK_ARGS: logs --follow os2iot-{{.SERVICE}}

dev:compose:
desc: Start containers in development mode
cmds:
- docker compose {{range .COMPOSE_FILES}} --file {{.}}{{end}} {{.TASK_ARGS}} {{.CLI_ARGS}}
vars:
COMPOSE_FILES:
- docker-compose.yml
- docker-compose.development.yml
Empty file modified configuration/os2iot-postgresql/initdb/001-init-os2iot.sh
100644 → 100755
Empty file.
Empty file modified configuration/os2iot-postgresql/initdb/002-init-os2iot-e2e.sh
100644 → 100755
Empty file.
Empty file modified configuration/postgres/initdb/001-init-chirpstack.sh
100644 → 100755
Empty file.
Empty file modified configuration/postgres/initdb/002-chirpstack_extensions.sh
100644 → 100755
Empty file.
19 changes: 19 additions & 0 deletions docker-compose.development.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
services:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use the nginx as proxy to have the same setup as production, where /api is proxied to the backend

os2iot-frontend:
# https://mherman.org/blog/dockerizing-an-angular-app/
build:
dockerfile: "Dockerfile"
volumes:
- '../OS2IoT-frontend:/app'
- '/app/node_modules'

os2iot-backend:
build:
target: dev
volumes:
- '../OS2IoT-backend:/app'
- '/app/node_modules'

nginx:
ports:
- "8888:80"
Loading