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
242 changes: 242 additions & 0 deletions LOCAL_DEVELOPMENT_HOWTO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
# Local Development Setup

This guide covers setting up **summit-admin** (React/webpack dev server) connected to local **summit-api** and **openstackid** instances.

## Prerequisites

- Docker and docker-compose
- Node.js / npm
- A MySQL client (mariadb or mysql CLI)
- Repos checked out locally:
- `openstackid` → runs on `http://localhost:8001`
- `summit-api` → runs on `http://localhost:8002`
- `summit-admin` → runs on `http://localhost:8080`

---

## 1. Start openstackid

Create the .env file and generate an app key:

```bash
cd /path/to/openstackid
cp .env.testing .env
php artisan key:generate
./start_local_server.sh
```

`start_local_server.sh` handles everything: composer install, Doctrine migrations (`doctrine:migrations:migrate`), database seeding, and super admin creation. No manual migration or seed commands are needed.

> The default super admin credentials created by the script are `test@test.com` / `1Qaz2wsx!`.

---

## 2. Create the Frontend JS Client in openstackid

Go to `http://localhost:8001`. You may get a permissions error writing to the logs directory. If so fix it like so:

```bash
docker exec idp-app chmod -R 777 /var/www/storage/logs
```

Create an OAuth2 client (OAUTH2 Console -> OAUTH2 Applications -> Register Application):

- **Application Type**: Client Side (JS)
- **Application Name**: `summit-admin-local` (or any name)
- **Token Endpoint Auth Method**: None
- **Active**: checked

After saving, edit the client and under Security Settings:

- **Use PKCE**: checked (only visible for JS Client type)

Note the generated **client_id**. Then set the redirect URI and allowed origin via SQL (the UI only accepts HTTPS URLs):

```bash
# Replace <client_db_id> with the integer ID from oauth2_client table. For a new setup this should be 1.
mysql -u idp_user -h 127.0.0.1 -P 30780 --password=1qaz2wsx! idp_local -e "
UPDATE oauth2_client
SET redirect_uris = 'http://localhost:8080/auth/callback',
allowed_origins = 'http://localhost:8080',
post_logout_redirect_uris = 'http://localhost:8080/auth/logout'
WHERE id = <client_db_id>;
"
```

Flush the Doctrine result cache:

```bash
docker exec openstackid-redis-1 redis-cli -a 1qaz2wsx! -n 5 FLUSHDB
```

---

## 3. Create the summit-api Resource Server Client

In the openstackid admin UI, create a second client:

- **Application Type**: Web Application
- **Application Name**: `summit-api-resource-server` (or any name)
- **Active**: checked

Note the generated **client_id** and **client_secret** — you will need them in step 5.

> After you run the SQL in step 5 to set `resource_server_id` on this client, it will disappear from the admin UI client list. This is expected — openstackid treats resource server clients as internal server-to-server credentials and hides them from the application list. The client is still present in the database and functioning correctly.

---

## 4. Seed openstackid Scopes

From the summit-admin repo root, edit `seed_idp_scopes.sql` and set `@client_id` to the integer DB id of the frontend JS client created in step 2. Then run:

```bash
mysql -u idp_user -h 127.0.0.1 -P 30780 --password=1qaz2wsx! idp_local < seed_idp_scopes.sql
```

This inserts the summit-api resource server, registers all API scopes, and grants them to the frontend client.

Now link the resource server client from step 3 to the summit-api resource server:

```bash
# Confirm the resource server id (should be 2 for summit-api)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid hardcoding expected resource server ID in docs.

“should be 2” is often false once a developer re-runs setup or has preexisting data; this can lead to wrong resource_server_id linkage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@LOCAL_DEVELOPMENT_HOWTO.md` at line 101, The comment at line 101 in
LOCAL_DEVELOPMENT_HOWTO.md hardcodes the expected resource server ID as 2, which
can vary based on setup or preexisting data and lead to incorrect
configurations. Instead of stating "should be 2 for summit-api," update the
documentation to guide developers on how to verify their actual resource server
ID by checking the database or API output, so they use the correct ID regardless
of their setup state.

mysql -u idp_user -h 127.0.0.1 -P 30780 --password=1qaz2wsx! idp_local -e "
SELECT id, friendly_name FROM oauth2_resource_server;
"

# Link the client to the resource server
mysql -u idp_user -h 127.0.0.1 -P 30780 --password=1qaz2wsx! idp_local -e "
UPDATE oauth2_client SET resource_server_id = <rs_id> WHERE app_name = 'summit-api-resource-server';
"
```

Flush the Doctrine result cache:

```bash
docker exec openstackid-redis-1 redis-cli -a 1qaz2wsx! -n 5 FLUSHDB
```

---

## 5. Configure and Start summit-api

Create the .env file and generate an app key:

```bash
cd /path/to/summit-api
cp .env.testing .env
php artisan key:generate
```

By default some of the ports used by summit-api will conflict with openstackid, so create a docker-compose.override.yml to move the ports:

```bash
cd /path/to/summit-api
cp docker-compose.override.testing.yml docker-compose.override.yml
```

Edit `/path/to/summit-api/.env` and set:

```dotenv
APP_OAUTH_2_0_AUTH_SERVER_BASE_URL=http://host.docker.internal:8001
APP_OAUTH_2_0_CLIENT_ID=<resource-server-client-id-from-step-4>
APP_OAUTH_2_0_CLIENT_SECRET=<resource-server-client-secret-from-step-4>
```

Then start the server:

```bash
./start_local_server.sh
```

Fix storage permissions (required on first run):

```bash
docker exec summit-api chmod -R 777 /var/www/storage
```

---

## 6. Create a Test Member in summit-api

The login flow requires a matching member record in the summit-api model DB with an admin group assigned. Connect to the model DB (port 32781):

```bash
mysql -u root -h 127.0.0.1 -P 32781 --password=1qaz2wsx! homestead << 'EOF'
INSERT INTO Member (ClassName, Created, LastEdited, FirstName, Surname, Email, Active, EmailVerified, ExternalUserIdentifier)
VALUES ('Member', NOW(), NOW(), 'Test', 'Admin', 'test@test.com', 1, 1, 'test@test.com');

SET @member_id = LAST_INSERT_ID();

INSERT INTO `Group` (Title, Code) VALUES ('super-admins', 'super-admins');
SET @group_id = LAST_INSERT_ID();

INSERT INTO Group_Members (GroupID, MemberID) VALUES (@group_id, @member_id);
EOF
Comment on lines +165 to +174

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the test member/group SQL snippet re-runnable.

These inserts are not idempotent. Re-running the guide can fail (or create duplicates) before Group_Members is inserted, breaking repeat local bootstrap.

Suggested direction
-INSERT INTO `Group` (Title, Code) VALUES ('super-admins', 'super-admins');
-SET `@group_id` = LAST_INSERT_ID();
+INSERT INTO `Group` (Title, Code)
+SELECT 'super-admins', 'super-admins'
+WHERE NOT EXISTS (SELECT 1 FROM `Group` WHERE Code = 'super-admins');
+SELECT ID INTO `@group_id` FROM `Group` WHERE Code = 'super-admins' LIMIT 1;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
INSERT INTO Member (ClassName, Created, LastEdited, FirstName, Surname, Email, Active, EmailVerified, ExternalUserIdentifier)
VALUES ('Member', NOW(), NOW(), 'Test', 'Admin', 'test@test.com', 1, 1, 'test@test.com');
SET @member_id = LAST_INSERT_ID();
INSERT INTO `Group` (Title, Code) VALUES ('super-admins', 'super-admins');
SET @group_id = LAST_INSERT_ID();
INSERT INTO Group_Members (GroupID, MemberID) VALUES (@group_id, @member_id);
EOF
INSERT INTO Member (ClassName, Created, LastEdited, FirstName, Surname, Email, Active, EmailVerified, ExternalUserIdentifier)
VALUES ('Member', NOW(), NOW(), 'Test', 'Admin', 'test@test.com', 1, 1, 'test@test.com');
SET `@member_id` = LAST_INSERT_ID();
INSERT INTO `Group` (Title, Code)
SELECT 'super-admins', 'super-admins'
WHERE NOT EXISTS (SELECT 1 FROM `Group` WHERE Code = 'super-admins');
SELECT ID INTO `@group_id` FROM `Group` WHERE Code = 'super-admins' LIMIT 1;
INSERT INTO Group_Members (GroupID, MemberID) VALUES (`@group_id`, `@member_id`);
EOF
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@LOCAL_DEVELOPMENT_HOWTO.md` around lines 165 - 174, The SQL snippet
containing the three INSERT statements (INSERT INTO Member, INSERT INTO Group,
and INSERT INTO Group_Members) is not idempotent and can fail or create
duplicates on re-runs. Modify this snippet to be re-runnable by either: deleting
existing test records before the inserts, or using conditional logic (such as
checking if records exist with specific values like email 'test@test.com' or
group code 'super-admins') before inserting, or using INSERT ... ON DUPLICATE
KEY UPDATE statements with appropriate unique constraints. This ensures the
bootstrap process can be safely repeated without creating duplicates or failing
partway through the insertion sequence.

```

Use the same email address as your openstackid user account.

---

## 7. Configure summit-admin

Copy or update `.env` with:

```dotenv
OAUTH2_CLIENT_ID=<frontend-client-id-from-step-2>
IDP_BASE_URL=http://localhost:8001
API_BASE_URL=http://localhost:8002
SCOPES_BASE_REALM=http://localhost:8002
SCOPES="profile openid offline_access ${SCOPES_BASE_REALM}/summits/read ${SCOPES_BASE_REALM}/summits/read/all ${SCOPES_BASE_REALM}/summits/write ${SCOPES_BASE_REALM}/summits/write-event ${SCOPES_BASE_REALM}/summits/delete-event ${SCOPES_BASE_REALM}/summits/publish-event ${SCOPES_BASE_REALM}/summits/write-presentation-materials ${SCOPES_BASE_REALM}/summits/registration-orders/update ${SCOPES_BASE_REALM}/summits/registration-orders/delete ${SCOPES_BASE_REALM}/summits/registration-orders/create/offline ${SCOPES_BASE_REALM}/summits/badge-scans/read ${SCOPES_BASE_REALM}/members/read ${SCOPES_BASE_REALM}/members/read/me ${SCOPES_BASE_REALM}/members/write ${SCOPES_BASE_REALM}/speakers/write ${SCOPES_BASE_REALM}/attendees/write ${SCOPES_BASE_REALM}/companies/read ${SCOPES_BASE_REALM}/companies/write ${SCOPES_BASE_REALM}/organizations/read ${SCOPES_BASE_REALM}/organizations/write ${SCOPES_BASE_REALM}/summit-administrator-groups/read ${SCOPES_BASE_REALM}/summit-administrator-groups/write ${SCOPES_BASE_REALM}/summit-media-file-types/read ${SCOPES_BASE_REALM}/summit-media-file-types/write ${SCOPES_BASE_REALM}/audit-logs/read"
```

> The SCOPES list is intentionally trimmed to only the scopes served by summit-api locally. External microservice scopes (email, purchases, sponsor, etc.) are excluded to avoid requiring those services running.

---

## 8. Start summit-admin

```bash
yarn install
yarn serve
```

Open `http://localhost:8080` and click **Login**.

---

## Troubleshooting

### IP banned by openstackid (404 "page is invalid" on login)

Repeated failed introspection attempts trigger openstackid's blacklist. Clear the ban:

```bash
docker exec openstackid-redis-1 redis-cli -a 1qaz2wsx! DEL "172.18.0.1" 2>/dev/null
docker exec idp-db-local mysql -u idp_user --password='1qaz2wsx!' idp_local -e "DELETE FROM banned_ips WHERE ip = '172.18.0.1';"
```
Comment on lines +215 to +217

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Generalize IP-ban cleanup instructions.

Using a fixed IP (172.18.0.1) is environment-specific and often won’t match the blocked address. The troubleshooting step should first query banned_ips and delete returned rows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@LOCAL_DEVELOPMENT_HOWTO.md` around lines 215 - 217, The IP-ban cleanup
instructions in LOCAL_DEVELOPMENT_HOWTO.md hardcode the IP address 172.18.0.1,
which is environment-specific and may not match the actual blocked IP address.
Revise the troubleshooting step to first query the banned_ips table to identify
which IPs are currently blocked, then provide instructions to delete those
returned rows from both the Redis cache (using redis-cli DEL) and the database
(using the DELETE FROM banned_ips query), replacing the hardcoded IP with a
variable or placeholder that users can substitute with their actual blocked IP
address.


### Permission denied on summit-api storage

```bash
docker exec summit-api chmod -R 777 /var/www/storage
```

### Doctrine proxy directory not writable

```bash
docker exec summit-api chmod -R 777 /var/www/storage/proxies
```

### Stale Doctrine result cache after openstackid DB changes

```bash
docker exec openstackid-redis-1 redis-cli -a 1qaz2wsx! -n 5 FLUSHDB
```

### Config changes not taking effect in summit-api

```bash
docker exec summit-api php artisan config:clear
docker restart summit-api
```
82 changes: 82 additions & 0 deletions seed_idp_scopes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
-- =============================================================
-- IDP scope seed for local summit-admin development
--
-- Run AFTER:
-- 1. openstackid db:seed (creates openid/offline_access/profile scopes)
-- 2. Creating your OAuth2 client in the openstackid admin UI
--
-- Usage (from host, adjust port if needed):
-- mysql -u idp_user -h 127.0.0.1 -P 30780 --password=1qaz2wsx! idp_local < seed_idp_scopes.sql
--
-- Verify your client DB id first:
-- SELECT id, client_id FROM oauth2_client;
-- Then set @client_id below accordingly.
-- =============================================================

SET @client_id = 1;

-- -------------------------------------------------------------
-- 1. Grant openstackid-native scopes (openid, offline_access, profile)
-- -------------------------------------------------------------
INSERT IGNORE INTO oauth2_client_api_scope (client_id, scope_id, created_at, updated_at)
SELECT @client_id, id, NOW(), NOW()
FROM oauth2_api_scope
WHERE name IN ('openid', 'offline_access', 'profile');

-- -------------------------------------------------------------
-- 2. Register summit-api as a resource server
-- -------------------------------------------------------------
INSERT INTO oauth2_resource_server (friendly_name, host, ips, active, created_at, updated_at)
VALUES ('summit-api', 'localhost', '127.0.0.1', 1, NOW(), NOW());

SET @rs_id = LAST_INSERT_ID();

INSERT INTO oauth2_api (name, description, active, resource_server_id, created_at, updated_at)
VALUES ('summit-api', 'Summit API', 1, @rs_id, NOW(), NOW());

SET @api_id = LAST_INSERT_ID();
Comment on lines +29 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Make the seed script idempotent across resource server/API/scope creation.

Only the client-scope grants are idempotent today. Re-running this script can create duplicate oauth2_resource_server / oauth2_api rows or fail on duplicate scope names, which makes local setup brittle and can drift IDs used downstream.

Suggested direction
-INSERT INTO oauth2_resource_server (friendly_name, host, ips, active, created_at, updated_at)
-VALUES ('summit-api', 'localhost', '127.0.0.1', 1, NOW(), NOW());
-
-SET `@rs_id` = LAST_INSERT_ID();
+INSERT INTO oauth2_resource_server (friendly_name, host, ips, active, created_at, updated_at)
+SELECT 'summit-api', 'localhost', '127.0.0.1', 1, NOW(), NOW()
+WHERE NOT EXISTS (
+  SELECT 1 FROM oauth2_resource_server WHERE friendly_name = 'summit-api'
+);
+SELECT id INTO `@rs_id` FROM oauth2_resource_server WHERE friendly_name = 'summit-api' LIMIT 1;
 
-INSERT INTO oauth2_api (name, description, active, resource_server_id, created_at, updated_at)
-VALUES ('summit-api', 'Summit API', 1, `@rs_id`, NOW(), NOW());
-
-SET `@api_id` = LAST_INSERT_ID();
+INSERT INTO oauth2_api (name, description, active, resource_server_id, created_at, updated_at)
+SELECT 'summit-api', 'Summit API', 1, `@rs_id`, NOW(), NOW()
+WHERE NOT EXISTS (
+  SELECT 1 FROM oauth2_api WHERE name = 'summit-api'
+);
+SELECT id INTO `@api_id` FROM oauth2_api WHERE name = 'summit-api' LIMIT 1;
 
-INSERT INTO oauth2_api_scope (...)
+INSERT IGNORE INTO oauth2_api_scope (...)
 VALUES (...);

Also applies to: 42-69, 74-77

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@seed_idp_scopes.sql` around lines 29 - 37, Make the seed script idempotent by
converting the INSERT statements for oauth2_resource_server, oauth2_api, and
oauth2_scope tables to use INSERT ... ON DUPLICATE KEY UPDATE or similar
idempotent pattern. At seed_idp_scopes.sql lines 29-37, update the INSERT
statements for oauth2_resource_server and oauth2_api to be idempotent using
appropriate unique constraints or conditional logic. Apply the same idempotent
pattern to the scope insertions at lines 42-69 and any remaining INSERT
statements at lines 74-77 to ensure the script can be safely re-run without
creating duplicate rows or failing on constraint violations.


-- -------------------------------------------------------------
-- 3. Insert summit-api scopes (matching APP_SCOPE_BASE_REALM=http://localhost:8002)
-- -------------------------------------------------------------
INSERT INTO oauth2_api_scope
(name, short_description, description, active, is_default, is_system, assigned_by_groups, api_id, created_at, updated_at)
VALUES
('http://localhost:8002/summits/read', 'Read Summit Data', 'Read Summit Data', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/summits/read/all', 'Read All Summit Data', 'Read All Summit Data', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/summits/write', 'Write Summit Data', 'Write Summit Data', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/summits/write-event', 'Write Summit Event', 'Write Summit Event', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/summits/delete-event', 'Delete Summit Event', 'Delete Summit Event', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/summits/publish-event', 'Publish Summit Event', 'Publish Summit Event', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/summits/write-presentation-materials', 'Write Presentation Materials', 'Write Presentation Materials', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/summits/registration-orders/update', 'Update Registration Orders', 'Update Registration Orders', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/summits/registration-orders/delete', 'Delete Registration Orders', 'Delete Registration Orders', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/summits/registration-orders/create/offline','Create Offline Registration Orders', 'Create Offline Registration Orders', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/summits/badge-scans/read', 'Read Badge Scans', 'Read Badge Scans', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/members/read', 'Read Member Data', 'Read Member Data', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/members/read/me', 'Read My Member Data', 'Read My Member Data', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/members/write', 'Write Member Data', 'Write Member Data', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/speakers/write', 'Write Speakers Data', 'Write Speakers Data', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/attendees/write', 'Write Attendees Data', 'Write Attendees Data', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/companies/read', 'Read Companies', 'Read Companies', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/companies/write', 'Write Companies', 'Write Companies', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/organizations/read', 'Read Organizations', 'Read Organizations', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/organizations/write', 'Write Organizations', 'Write Organizations', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/summit-administrator-groups/read', 'Read Summit Admin Groups', 'Read Summit Admin Groups', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/summit-administrator-groups/write', 'Write Summit Admin Groups', 'Write Summit Admin Groups', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/summit-media-file-types/read', 'Read Summit Media File Types', 'Read Summit Media File Types', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/summit-media-file-types/write', 'Write Summit Media File Types', 'Write Summit Media File Types', 1, 0, 0, 0, @api_id, NOW(), NOW()),
('http://localhost:8002/audit-logs/read', 'Read Audit Logs', 'Read Audit Logs', 1, 0, 0, 0, @api_id, NOW(), NOW());

-- -------------------------------------------------------------
-- 4. Grant all summit-api scopes to the client
-- -------------------------------------------------------------
INSERT IGNORE INTO oauth2_client_api_scope (client_id, scope_id, created_at, updated_at)
SELECT @client_id, id, NOW(), NOW()
FROM oauth2_api_scope
WHERE api_id = @api_id;

-- -------------------------------------------------------------
-- 5. Flush Redis cache for this client (run in openstackid container):
-- redis-cli -h redis -a 1qaz2wsx! KEYS "*oauth2*client*" | xargs redis-cli -h redis -a 1qaz2wsx! DEL
-- -------------------------------------------------------------
Loading