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
9 changes: 1 addition & 8 deletions Taskfile.docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ tasks:
aliases: [ default ]
cmds:
- task: build
- task: admin
- task: watch

build:
Expand Down Expand Up @@ -39,13 +38,7 @@ tasks:

cli:
desc: Start shell in cli container
cmds:
- $COMPOSE_CMD run cli

admin:
desc: Create an admin user
cmds:
- $COMPOSE_CMD run cli node cli/dist/server createSiteAdmin {{ .ADMIN_MAIL }} {{ .ADMIN_ORG }} {{ .ADMIN_PASS }}
cmds: [ $COMPOSE_CMD run cli ]

clean:
desc: Stop the application and remove data
Expand Down
1 change: 1 addition & 0 deletions bin/api
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env sh
set -eu

echo "Starting API ..."
exec node api/dist/server
1 change: 1 addition & 0 deletions bin/cli
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env sh
set -eu

echo "Starting CLI ..."
exec node cli/dist/server
1 change: 1 addition & 0 deletions bin/entrypoint
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env sh
set -eu

MONGO_HOST="${MONGO_HOST:-localhost}"
MONGO_PORT="${MONGO_PORT:-27017}"
Expand Down
5 changes: 5 additions & 0 deletions bin/migrations
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env sh
set -eu

echo "Executing database migrations ..."
node cli/dist/server migrateMongo --up
5 changes: 5 additions & 0 deletions bin/seeds
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env sh
set -eu

echo "Setting up seed data ..."
node cli/dist/server createSiteAdmin "${LL_ADMIN_EMAIL:?}" "${LL_ADMIN_ORG:?}" "${LL_ADMIN_PASSWORD:?}"
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify idempotency/upsert semantics of createSiteAdmin

The PR objective requires creating or upserting an admin on startup. Please confirm the CLI operation is idempotent (no duplicate users, safe on re-runs).

Run this script to locate the implementation and check for upsert/exists handling:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for CLI command createSiteAdmin definition and usage..."
rg -n -C3 -S '\bcreateSiteAdmin\b'

echo
echo "Scanning for idempotent patterns near user creation (upsert/exists/unique)..."
rg -n -C3 -iP 'upsert|onConflict|unique|ensureIndex|already exists|duplicate key|findOne|findUnique|findFirst'

echo
echo "If Prisma is used, look for upsert calls:"
rg -n -C3 -iP '\bupsert\s*\('

echo
echo "If Mongoose is used, check for findOneAndUpdate with upsert: true:"
rg -n -C3 -iP 'findOneAndUpdate\s*\(.*upsert\s*:\s*true'

Length of output: 133165


🏁 Script executed:

#!/bin/bash
# Display the key parts of createSiteAdmin implementation to verify idempotency
sed -n '1,200p' cli/src/commands/createSiteAdmin.js

Length of output: 3909


Fix idempotency in createSiteAdmin

The current implementation of createSiteAdmin is not idempotent because:

  • Organisation assignment always pushes a duplicate entry on every run.
  • Password‐force update silently fails to persist when the user is already a SITE_ADMIN.

Please update cli/src/commands/createSiteAdmin.js:

  • In ensureUserHasOrg (≈line 39), compare IDs—not name—to detect an existing org:
- const foundOrg = _.find(user.organisations, org => org.toString() === organisation.name);
+ const foundOrg = _.find(
+   user.organisations,
+   orgId => orgId.toString() === organisation._id.toString()
+ );
  • In ensureSiteAdmin (≈line 23), always save when forcing a password update (even if the SITE_ADMIN scope already exists):
- async function ensureSiteAdmin(user, password, forceUpdatePassword) {
-   if (forceUpdatePassword) user.password = password;
-   if (!_.includes(user.scopes, scopes.SITE_ADMIN)) {
-     user.scopes.push(scopes.SITE_ADMIN);
-     return await user.save();
-   }
-   return user;
- }
+ async function ensureSiteAdmin(user, password, forceUpdatePassword) {
+   let modified = false;
+   if (forceUpdatePassword) {
+     user.password = password;
+     modified = true;
+   }
+   if (!_.includes(user.scopes, scopes.SITE_ADMIN)) {
+     user.scopes.push(scopes.SITE_ADMIN);
+     modified = true;
+   }
+   if (modified) {
+     await user.save();
+   }
+   return user;
+ }

With these changes, rerunning bin/seeds will no longer accumulate duplicate organisations or drop intended password updates.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In cli/src/commands/createSiteAdmin.js around lines 23 and 39, fix idempotency:
in ensureUserHasOrg (≈line 39) check existing user.organisations by
organisation.id (not by name) and only push the org object if no matching id
exists to avoid duplicate entries; in ensureSiteAdmin (≈line 23) when
force-password is requested, always assign the new password and call await
user.save() even if the SITE_ADMIN scope is already present so the password
change persists. Ensure both checks use strict ID comparison and await any save
calls.

Copy link
Author

Choose a reason for hiding this comment

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

  • Duplicate organisation: Probably bad, should be investigated
  • Not updating password: Actually the intended behaviour once the account exists

1 change: 1 addition & 0 deletions bin/ui
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env sh
set -eu

echo "Starting UI ..."
exec node ui/dist/server
1 change: 1 addition & 0 deletions bin/worker
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env sh
set -eu

echo "Starting worker ..."
exec node worker/dist/server
83 changes: 58 additions & 25 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@ name: learninglocker
x-common: &common
build: .
image: ${IMAGE_NAME_PREFIX:-ghcr.io/ude-soco/}learninglocker:${IMAGE_TAG:-latest}
depends_on:
mongo:
condition: service_healthy
redis:
condition: service_healthy
develop:
watch:
- path: package.json
Expand All @@ -33,57 +28,95 @@ x-common: &common
# SMTP_USER
# SMTP_PASS
APP_SECRET: "i-am-not-secure-please-change-me"
LL_ADMIN_EMAIL: "admin@mail.com"
LL_ADMIN_ORG: "soco"
LL_ADMIN_PASSWORD: "1234qweR"

services:
api:
ui:
<<: *common
command: api
ports: [ "8080:8080" ]
command: ui
ports: ["3000:3000"]
depends_on:
- mongo
api:
condition: service_healthy
develop:
watch:
- path: /api
- path: /ui
action: rebuild

ui:
api:
<<: *common
command: ui
ports: [ "3000:3000" ]
command: api
ports: ["8080:8080"]
healthcheck:
test: [CMD, node, -e, "'fetch(`http://localhost:8080/`).then(x=>x.status==200?process.exit(0):process.exit(1)).catch(()=>process.exit(1))'"]
start_period: 10s
interval: 10s
depends_on:
mongo:
condition: service_healthy
restart: true
seeds:
condition: service_completed_successfully
redis:
condition: service_healthy
restart: true
develop:
watch:
- path: /ui
- path: /api
action: rebuild

worker:
<<: *common
command: worker
depends_on:
- mongo
mongo:
condition: service_healthy
restart: true
redis:
condition: service_healthy
restart: true
develop:
watch:
- path: /worker
action: rebuild

cli:
<<: *common
profiles: [ halted ]
command: bash
depends_on:
- mongo
profiles: [halted]
command: cli
tty: true
develop:
watch:
- path: /cli
action: rebuild

seeds:
<<: *common
command: seeds
depends_on:
migrations:
condition: service_completed_successfully
restart: on-failure

migrations:
<<: *common
command: migrations
depends_on:
mongo:
condition: service_healthy
restart: true
restart: on-failure

mongo:
image: mongo:6.0
image: mongo:4.4
command: mongod --wiredTigerCacheSizeGB 0.25 --quiet --logpath /dev/null
ports: [ "27017:27017" ]
ports: ["27017:27017"]
volumes:
- mongo:/data/db
healthcheck:
test: "mongosh --nodb --eval 'disableTelemetry()'; echo 'db.runCommand({ping: 1}).ok' | mongosh localhost:27017/test --quiet"
test: "mongo --nodb --eval 'disableTelemetry()'; echo 'db.runCommand({ping: 1}).ok' | mongo localhost:27017/test --quiet"
start_period: 2s
interval: 10s

Expand All @@ -92,16 +125,16 @@ services:
command: --loglevel warning
volumes:
- redis:/data
ports: [ "6379:6379" ]
ports: ["6379:6379"]
healthcheck:
test: ["CMD", "redis-cli","ping"]
test: ["CMD", "redis-cli", "ping"]
start_period: 2s
interval: 10s

xapi-service:
image: learninglocker/xapi-service
platform: linux/amd64
ports: [ "8081:8081" ]
ports: ["8081:8081"]
depends_on:
mongo:
condition: service_healthy
Expand Down