Compare commits
10 Commits
929f08b656
...
bd5759f037
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd5759f037 | ||
|
|
872e381c00 | ||
|
|
64402e8e2a | ||
|
|
88b11b8318 | ||
|
|
65e0477761 | ||
|
|
98736ae1bc | ||
|
|
7308a4773d | ||
|
|
99541f0f9d | ||
|
|
f950c22a59 | ||
|
|
b63f8eed0c |
@@ -1,45 +1,47 @@
|
||||
.svelte-kit
|
||||
.vite
|
||||
node_modules
|
||||
dist
|
||||
build
|
||||
.cache
|
||||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
.git
|
||||
# Build artifacts and local state
|
||||
.svelte-kit/
|
||||
.vite/
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
.cache/
|
||||
|
||||
# VCS and IDE
|
||||
.git/
|
||||
.gitignore
|
||||
coverage
|
||||
tests
|
||||
documentation
|
||||
backups
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS junk
|
||||
.DS_Store
|
||||
.directory
|
||||
|
||||
# Logs and temp files
|
||||
*.log
|
||||
*.bak
|
||||
*.tgz
|
||||
.env
|
||||
.env.*
|
||||
npm-debug.log
|
||||
package-lock.json.bak
|
||||
yarn-error.log
|
||||
/.cache
|
||||
/.parcel-cachenode_modules/
|
||||
build/
|
||||
.svelte-kit/
|
||||
.git/
|
||||
.env
|
||||
.env.*
|
||||
!.env.staging
|
||||
!.env.prod
|
||||
npm_deploy/
|
||||
test-results/
|
||||
test_results/
|
||||
documentation/
|
||||
backups/
|
||||
*.log
|
||||
*.bak
|
||||
.claude/
|
||||
.vscode/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Test output and dev-only dirs
|
||||
tests/
|
||||
test-results/
|
||||
test_results/
|
||||
coverage/
|
||||
documentation/
|
||||
backups/
|
||||
.claude/
|
||||
|
||||
# Deployment artifacts
|
||||
npm_deploy/
|
||||
package-lock.json.bak
|
||||
|
||||
# Env files: exclude all live secrets, allow only the per-environment files needed for Docker builds.
|
||||
# .env.local is workstation-only and must never enter a container image.
|
||||
.env
|
||||
.env.*
|
||||
!.env.dev
|
||||
!.env.test
|
||||
!.env.prod
|
||||
|
||||
19
.env.dev.default
Normal file
19
.env.dev.default
Normal file
@@ -0,0 +1,19 @@
|
||||
# One Sky IT's Aether Framework and System — DEV template (dev-*.oneskyit.com)
|
||||
# Copy to .env.dev and fill in real values.
|
||||
|
||||
|
||||
# Aether API access
|
||||
PUBLIC_AE_API_PROTOCOL=https
|
||||
PUBLIC_AE_API_SERVER=dev-api.oneskyit.com
|
||||
PUBLIC_AE_API_BAK_SERVER=test-api.oneskyit.com
|
||||
PUBLIC_AE_API_PORT=443
|
||||
PUBLIC_AE_API_PATH=
|
||||
PUBLIC_AE_API_SECRET_KEY=XXXX
|
||||
PUBLIC_AE_API_CRUD_SUPER_KEY=XXXX
|
||||
|
||||
|
||||
# Bootstrap key: used only for the unauthenticated site-domain lookup on first load.
|
||||
# Separate from the main API key — has limited permissions (no account_id required).
|
||||
PUBLIC_AE_BOOTSTRAP_KEY=XXXX
|
||||
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# One Sky IT's Aether Framework and System — PRODUCTION template
|
||||
# One Sky IT's Aether Framework and System — PROD template (api.oneskyit.com)
|
||||
# Copy to .env.prod and fill in real values.
|
||||
# AE_CFG_ID: 1=Default, 5=Home Dev, 7=Live Testing/Prod
|
||||
|
||||
# Shared config record (controls SMTP, API routing, external keys from DB)
|
||||
AE_CFG_ID=7
|
||||
|
||||
# Aether API access
|
||||
PUBLIC_AE_API_PROTOCOL=https
|
||||
@@ -13,16 +9,10 @@ PUBLIC_AE_API_PORT=443
|
||||
PUBLIC_AE_API_PATH=
|
||||
PUBLIC_AE_API_SECRET_KEY=XXXX
|
||||
PUBLIC_AE_API_CRUD_SUPER_KEY=XXXX
|
||||
|
||||
|
||||
|
||||
# Bootstrap key: used only for the unauthenticated site-domain lookup on first load.
|
||||
# Separate from the main API key — has limited permissions (no account_id required).
|
||||
PUBLIC_AE_BOOTSTRAP_KEY=XXXX
|
||||
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here
|
||||
PUBLIC_AE_NO_ACCOUNT_ID_TOKEN=Nothing_to_see_here
|
||||
|
||||
# SvelteKit app config
|
||||
AE_APP_NODE_PORT=3001
|
||||
|
||||
# Default demo/client context (set to the target account for this deployment)
|
||||
PUBLIC_AE_ACCOUNT_ID=XXXX
|
||||
PUBLIC_AE_EVENT_ID=XXXX
|
||||
PUBLIC_AE_SPONSORSHIP_CFG_ID=XXXX
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# One Sky IT's Aether Framework and System — STAGING / HOME DEV template
|
||||
# Copy to .env.staging and fill in real values.
|
||||
# AE_CFG_ID: 1=Default, 5=Home Dev, 7=Live Testing/Prod
|
||||
|
||||
# Shared config record (controls SMTP, API routing, external keys from DB)
|
||||
AE_CFG_ID=5
|
||||
|
||||
# Aether API access
|
||||
PUBLIC_AE_API_PROTOCOL=https
|
||||
PUBLIC_AE_API_SERVER=dev-api.oneskyit.com
|
||||
PUBLIC_AE_API_BAK_SERVER=test-api.oneskyit.com
|
||||
PUBLIC_AE_API_PORT=443
|
||||
PUBLIC_AE_API_PATH=
|
||||
PUBLIC_AE_API_SECRET_KEY=XXXX
|
||||
PUBLIC_AE_API_CRUD_SUPER_KEY=XXXX
|
||||
# Bootstrap key: used only for the unauthenticated site-domain lookup on first load.
|
||||
# Separate from the main API key — has limited permissions (no account_id required).
|
||||
PUBLIC_AE_BOOTSTRAP_KEY=XXXX
|
||||
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here
|
||||
PUBLIC_AE_NO_ACCOUNT_ID_TOKEN=Nothing_to_see_here
|
||||
|
||||
# SvelteKit app config
|
||||
AE_APP_NODE_PORT=3001
|
||||
|
||||
# Default demo/client context (set to the target account for this deployment)
|
||||
PUBLIC_AE_ACCOUNT_ID=XXXX # OSIT = _XY7DXtc9MY
|
||||
PUBLIC_AE_EVENT_ID=XXXX # OSIT = pjrcghqwert
|
||||
PUBLIC_AE_SPONSORSHIP_CFG_ID=XXXX
|
||||
18
.env.test.default
Normal file
18
.env.test.default
Normal file
@@ -0,0 +1,18 @@
|
||||
# One Sky IT's Aether Framework and System — TEST template (test-api.oneskyit.com)
|
||||
# Copy to .env.test and fill in real values.
|
||||
|
||||
# Aether API access
|
||||
PUBLIC_AE_API_PROTOCOL=https
|
||||
PUBLIC_AE_API_SERVER=test-api.oneskyit.com
|
||||
PUBLIC_AE_API_BAK_SERVER=api.oneskyit.com
|
||||
PUBLIC_AE_API_PORT=443
|
||||
PUBLIC_AE_API_PATH=
|
||||
PUBLIC_AE_API_SECRET_KEY=XXXX
|
||||
PUBLIC_AE_API_CRUD_SUPER_KEY=XXXX
|
||||
|
||||
|
||||
|
||||
# Bootstrap key: used only for the unauthenticated site-domain lookup on first load.
|
||||
# Separate from the main API key — has limited permissions (no account_id required).
|
||||
PUBLIC_AE_BOOTSTRAP_KEY=XXXX
|
||||
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,7 +8,8 @@ node_modules
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.prod.default
|
||||
!.env.staging.default
|
||||
!.env.test.default
|
||||
!.env.dev.default
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
|
||||
24
Dockerfile
24
Dockerfile
@@ -9,18 +9,32 @@ RUN npm install
|
||||
# Copy the rest of the source code.
|
||||
COPY . .
|
||||
|
||||
# Build Argument to determine build environment (staging, prod, or production).
|
||||
ARG BUILD_MODE=staging
|
||||
# Build Argument to determine build environment (dev, test, prod).
|
||||
ARG BUILD_MODE=dev
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Sync the SvelteKit project to generate ./.svelte-kit/tsconfig.json
|
||||
RUN npx svelte-kit sync
|
||||
|
||||
# Perform the build based on the BUILD_MODE argument.
|
||||
# Each script uses vite --mode <name>, which reads .env.<name> directly — no cp hack needed.
|
||||
RUN if [ "$BUILD_MODE" = "prod" ] || [ "$BUILD_MODE" = "production" ]; then \
|
||||
npm run build:prod; \
|
||||
elif [ "$BUILD_MODE" = "test" ]; then \
|
||||
npm run build:test; \
|
||||
else \
|
||||
npm run build:staging; \
|
||||
npm run build:dev; \
|
||||
fi
|
||||
|
||||
# Copy the source env file to .env.runtime for the deploy stage.
|
||||
# PUBLIC_* vars are baked into the JS bundle by vite; non-PUBLIC vars (AE_CFG_ID,
|
||||
# AE_APP_NODE_PORT) are read by the Node server at runtime and need this file.
|
||||
RUN if [ "$BUILD_MODE" = "prod" ] || [ "$BUILD_MODE" = "production" ]; then \
|
||||
cp .env.prod .env.runtime; \
|
||||
elif [ "$BUILD_MODE" = "test" ]; then \
|
||||
cp .env.test .env.runtime; \
|
||||
else \
|
||||
cp .env.dev .env.runtime; \
|
||||
fi
|
||||
|
||||
# Stage 2: Final runtime image
|
||||
@@ -35,8 +49,8 @@ COPY --from=builder /app/package-lock.json .
|
||||
# Install only production dependencies.
|
||||
RUN npm install --omit=dev
|
||||
|
||||
# Copy the resulting .env.production file to .env.
|
||||
COPY --from=builder /app/.env.production .env
|
||||
# Copy the runtime env file (non-PUBLIC vars for the Node server).
|
||||
COPY --from=builder /app/.env.runtime .env
|
||||
|
||||
# SvelteKit (via adapter-node) defaults to port 3000.
|
||||
EXPOSE 3000
|
||||
|
||||
62
README.md
62
README.md
@@ -123,32 +123,45 @@ Developer sandbox pages — not for production use.
|
||||
|
||||
# How to build and deploy SvelteKit:
|
||||
|
||||
The deployment is now fully integrated into the unified **Aether Docker Environment** (`aether_container_env`). The application is built directly from source inside a clean Docker container, ensuring consistent environment handling across staging and production.
|
||||
The deployment is fully integrated into the unified **Aether Docker Environment** (`aether_container_env`). The application is built inside a clean Docker container using `vite build --mode <env>`, which reads the corresponding `.env.<env>` file for `PUBLIC_` variables.
|
||||
|
||||
### Deployment Commands
|
||||
## Environments
|
||||
|
||||
Run these commands from the root of the `aether_app_sveltekit` project:
|
||||
| Environment | Env file | Vite mode | API server |
|
||||
| ----------- | ----------- | --------- | ------------------------- |
|
||||
| dev | `.env.dev` | `dev` | `dev-api.oneskyit.com` |
|
||||
| test | `.env.test` | `test` | `test-api.oneskyit.com` |
|
||||
| prod | `.env.prod` | `prod` | `api.oneskyit.com` |
|
||||
|
||||
## Commands (from `aether_app_sveltekit/`)
|
||||
|
||||
#### 1. Deploy to Staging (Dev Workstation)
|
||||
This triggers an autonomous build inside the Docker container using your `.env.staging` file and restarts the service.
|
||||
```bash
|
||||
npm run deploy:staging
|
||||
```
|
||||
# Active development — Vite HMR, no Docker
|
||||
npm run dev
|
||||
|
||||
#### 2. Deploy to Production
|
||||
This builds the image using production flags (using `.env.prod`) and restarts the production container.
|
||||
```bash
|
||||
npm run deploy:prod
|
||||
# Build Vite output only (no Docker)
|
||||
npm run build:dev
|
||||
npm run build:test
|
||||
npm run build:prod
|
||||
|
||||
# Build Docker image and restart container locally
|
||||
npm run build:docker:dev
|
||||
npm run build:docker:test
|
||||
npm run build:docker:prod
|
||||
|
||||
# Deploy to remote server (SSH → linode.oneskyit.com → deploy.sh)
|
||||
npm run deploy:remote:test
|
||||
npm run deploy:remote:prod
|
||||
```
|
||||
|
||||
### Technical Details
|
||||
|
||||
- **Unified Orchestration**: All services (API, UI, Redis) are managed via `~/OSIT_dev/aether_container_env/docker-compose.yml`.
|
||||
- **Dockerfile**: Uses a multi-stage build. Stage 1 (builder) installs dependencies and builds the app using the `BUILD_MODE` argument passed from Docker Compose. Stage 2 (runtime) creates the final lightweight image.
|
||||
- **Dockerfile**: Multi-stage build. Stage 1 (builder) runs `vite build --mode $BUILD_MODE` using `.env.$BUILD_MODE`. Stage 2 (runtime) creates the final lightweight Node image.
|
||||
- **Environment Handling**:
|
||||
- `PUBLIC_` variables are baked into the image during the build step.
|
||||
- Private runtime variables are passed via the orchestration's `.env` file.
|
||||
- **Networking**: The frontend communicates with the backend via the high-speed internal Docker network (`http://ae_api:5005`).
|
||||
- `PUBLIC_` variables are baked into the image at build time via the `.env.<mode>` file.
|
||||
- Private runtime variables are passed via the Docker Compose `.env` file in `aether_container_env/`.
|
||||
- **Remote deploy**: `aether_container_env/deploy.sh` handles git pull + Docker build + restart on the server. Triggered via `npm run deploy:remote:*`.
|
||||
|
||||
---
|
||||
|
||||
@@ -229,8 +242,9 @@ npm install @tiptap/extension-link @tiptap/extension-bullet-list @tiptap/extensi
|
||||
|
||||
The application uses standard SvelteKit `.env` files for build-time configuration (specifically for `PUBLIC_` prefixed variables).
|
||||
|
||||
- **`.env.staging`**: Used by `npm run deploy:staging`.
|
||||
- **`.env.prod`**: Used by `npm run deploy:prod`.
|
||||
- **`.env.dev`**: Used by `npm run build:docker:dev` and `npm run build:dev`.
|
||||
- **`.env.test`**: Used by `npm run build:docker:test` and `npm run build:test`.
|
||||
- **`.env.prod`**: Used by `npm run build:docker:prod` and `npm run build:prod`.
|
||||
- **`.env.local`**: Used during local development (`npm run dev`).
|
||||
|
||||
**Note:** Runtime variables (like private API keys or DB credentials) are managed in the deployment directory's `.env` file and passed to the containers via Docker Compose.
|
||||
@@ -248,16 +262,16 @@ npm run dev
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Deployment (Production/Staging)
|
||||
|
||||
To create a production or staging version and deploy it to the local containers:
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
# For Staging/Dev
|
||||
npm run deploy:staging
|
||||
# Build Docker image locally and restart container
|
||||
npm run build:docker:dev
|
||||
npm run build:docker:prod
|
||||
|
||||
# For Production
|
||||
npm run deploy:prod
|
||||
# Deploy to remote server (linode.oneskyit.com)
|
||||
npm run deploy:remote:test
|
||||
npm run deploy:remote:prod
|
||||
```
|
||||
|
||||
These commands use the multi-stage **Dockerfile** to build the app in a clean environment and automatically restart the corresponding Docker containers.
|
||||
|
||||
@@ -540,6 +540,23 @@ ae_loc.idaa_loc = { novi_uuid: 'test-uuid-value', ... };
|
||||
- [Naming Conventions](./AE__Naming_Conventions.md)
|
||||
- [Playwright Test README](../tests/README.md)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## IDAA Novi Groups and Moderators
|
||||
IDAA Couples Meeting = "e9e162f0-3d03-4241-9682-340135ec3fb8"
|
||||
|
||||
"Gregory X Boehm" "00ee764c-7559-496b-9d18-40d3e9092c0c"
|
||||
"Kee B. PARK" "24ab3297-bfce-473c-9311-4b31e3a8974f"
|
||||
"Laura Lander" "ac697456-61fe-4f7d-a8b8-d04866032320"
|
||||
"Nancy J Duff-Boehm" "5c7c09bc-4f23-432c-bfd9-87a66b548502"
|
||||
"Owen Lander" "9671a2c4-ff95-48c2-bcde-5c6eba95cded"
|
||||
"Susan Park" "4a9f94c5-d766-4808-ab76-117c9e43903a"
|
||||
|
||||
"Student/Resident Meeting Moderators" "d76d2c00-962d-40f6-a2e8-ed9c85594d96"
|
||||
"Melissa Eve Valasky" "182d1db3-caa9-41bc-b04a-2facc6859aeb"
|
||||
"Steven L. Klein" "5724aad7-6d89-47e7-8943-966fd22911bd"
|
||||
|
||||
---
|
||||
|
||||
**Document Status:** ✅ Current
|
||||
|
||||
@@ -73,7 +73,7 @@ Modify data in the system.
|
||||
> [!IMPORTANT]
|
||||
> **V3 responses always use random string IDs — never database integers.**
|
||||
|
||||
After a successful `POST` create or any `GET`, the response contains:
|
||||
All V3 responses — `POST` create, `GET` single, `GET` list, search, and `PATCH` update — contain:
|
||||
|
||||
| Field | Type | Use |
|
||||
| :--- | :--- | :--- |
|
||||
@@ -262,7 +262,150 @@ Frontend guidance:
|
||||
|
||||
---
|
||||
|
||||
## 7. Event Exhibit Tracking Export (Leads Export)
|
||||
## 7. User Actions (`/v3/action/user/`)
|
||||
|
||||
Stateful user account operations that are not standard CRUD. All require `x-aether-api-key`.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Migration from legacy `/user/*` routes:** The table below maps each legacy endpoint to its V3 replacement. Run both in parallel during transition; remove legacy routes once traffic logs confirm they are quiet.
|
||||
>
|
||||
> | Legacy | V3 Replacement |
|
||||
> |---|---|
|
||||
> | `GET /user/authenticate` | `POST /v3/action/user/authenticate` |
|
||||
> | `POST /user/verify_password` | `POST /v3/action/user/verify_password` |
|
||||
> | `PATCH /user/{id}/change_password` | `POST /v3/action/user/{id}/change_password` |
|
||||
> | `GET /user/{id}/new_auth_key` | `GET /v3/action/user/{id}/new_auth_key` |
|
||||
> | `GET /user/{id}/email_auth_key_url` | `GET /v3/action/user/{id}/email_auth_key_url` |
|
||||
> | `GET /user/lookup` | `POST /v3/crud/user/search` |
|
||||
> | `GET /user/lookup_email` | `POST /v3/crud/user/search` |
|
||||
> | `GET /user/lookup_username` | `POST /v3/crud/user/search` |
|
||||
|
||||
### A. Authenticate
|
||||
|
||||
Authenticate a user by **username + password** or **user_id + auth_key**.
|
||||
|
||||
- **Method:** `POST`
|
||||
- **Path:** `/v3/action/user/authenticate`
|
||||
- **Auth:** `x-aether-api-key` + `x-account-id` (scopes username lookups to the correct account)
|
||||
- **Security improvement:** Credentials are in the **POST body**, not query params — safe from URL logging.
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{ "username": "scott", "password": "MyPassword123!" }
|
||||
```
|
||||
or:
|
||||
```json
|
||||
{ "user_id": "<user_id_random>", "auth_key": "<one_time_key>", "valid_email": true }
|
||||
```
|
||||
|
||||
- `valid_email` (optional `bool`): if `true`, marks `email_verified = true` on success.
|
||||
- `inc_user_role_list` (optional query param, default `false`): include role list in the returned user object.
|
||||
|
||||
**Response on success:** Full user object (same shape as `GET /v3/crud/user/{id}`).
|
||||
|
||||
**Errors:** `400` missing credentials, `403` wrong password or account disabled, `404` user not found.
|
||||
|
||||
> **Auth key flow:** Auth keys are one-time-use — the key is cleared from the DB immediately on successful authentication. Request a new one via `GET /v3/action/user/{id}/new_auth_key`.
|
||||
|
||||
---
|
||||
|
||||
### B. Verify Password
|
||||
|
||||
Check a user's current password without changing it.
|
||||
|
||||
- **Method:** `POST`
|
||||
- **Path:** `/v3/action/user/verify_password`
|
||||
- **Auth:** `x-aether-api-key` + `x-account-id`
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{ "user_id": "<user_id_random>", "current_password": "MyPassword123!" }
|
||||
```
|
||||
or use `"username"` instead of `"user_id"` to look up by username within the account.
|
||||
|
||||
**Response:** `data: true` on match. `403` on mismatch, `404` if user not found.
|
||||
|
||||
---
|
||||
|
||||
### C. Change Password
|
||||
|
||||
Change a user's password. Optionally verify the current password first.
|
||||
|
||||
- **Method:** `POST`
|
||||
- **Path:** `/v3/action/user/{user_id}/change_password`
|
||||
- **Auth:** `x-aether-api-key` + `x-account-id`
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{ "new_password": "NewPassword456!", "current_password": "MyPassword123!" }
|
||||
```
|
||||
|
||||
- `new_password` is required (minimum 10 characters).
|
||||
- `current_password` is optional. If provided, it is verified before the change is applied. Omit it for admin-driven resets.
|
||||
|
||||
**Response:** `data: true` on success. `403` if `current_password` provided but wrong.
|
||||
|
||||
---
|
||||
|
||||
### D. Generate New Auth Key
|
||||
|
||||
Generate a fresh one-time-use auth key for the user and write it to the DB.
|
||||
|
||||
- **Method:** `GET`
|
||||
- **Path:** `/v3/action/user/{user_id}/new_auth_key`
|
||||
- **Auth:** `x-aether-api-key` + `x-account-id`
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{ "data": { "auth_key": "<new_key>" } }
|
||||
```
|
||||
|
||||
The returned key can then be passed to `/authenticate` (as `auth_key`) or embedded in a login URL. The user record must have `allow_auth_key = true` for key-based authentication to work.
|
||||
|
||||
---
|
||||
|
||||
### E. Email Auth Key URL
|
||||
|
||||
Generate a new auth key and email a one-time login link to the user's email address.
|
||||
|
||||
- **Method:** `GET`
|
||||
- **Path:** `/v3/action/user/{user_id}/email_auth_key_url`
|
||||
- **Auth:** `x-aether-api-key` + `x-account-id`
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `root_url` | `string` | `null` | Base URL the login link is built from. |
|
||||
| `key_param_name` | `string` | `auth_key` | Query param name used for the auth key in the generated link. |
|
||||
|
||||
**Response:** `data: true` on success (email sent). `500` if delivery failed (check account email config and that the user account is enabled with `allow_auth_key = true`).
|
||||
|
||||
---
|
||||
|
||||
### F. User Lookups via V3 CRUD Search
|
||||
|
||||
The three legacy lookup routes (`lookup`, `lookup_email`, `lookup_username`) are replaced by standard V3 CRUD search:
|
||||
|
||||
```typescript
|
||||
// Look up by user_id (Vision ID)
|
||||
POST /v3/crud/user/search
|
||||
{ "and": [{ "field": "id_random", "op": "eq", "value": "<user_id>" }] }
|
||||
|
||||
// Look up by email
|
||||
POST /v3/crud/user/search
|
||||
{ "and": [{ "field": "email", "op": "eq", "value": "user@example.com" }] }
|
||||
|
||||
// Look up by username
|
||||
POST /v3/crud/user/search
|
||||
{ "and": [{ "field": "username", "op": "eq", "value": "scott" }] }
|
||||
```
|
||||
|
||||
Results are automatically scoped to the `x-account-id` provided in the request.
|
||||
|
||||
---
|
||||
|
||||
## 9. Event Exhibit Tracking Export (Leads Export)
|
||||
|
||||
Allows an exhibitor to download all lead-capture records for their exhibit as a CSV or XLSX file.
|
||||
|
||||
@@ -330,7 +473,7 @@ const url = URL.createObjectURL(blob);
|
||||
|
||||
---
|
||||
|
||||
## 8. Troubleshooting 403 Forbidden
|
||||
## 10. Troubleshooting 403 Forbidden
|
||||
|
||||
If you receive a 403 on a valid ID:
|
||||
1. Verify `x-aether-api-key` is correct.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- **Zero Tolerance:** If a task introduces even a single svelte-check warning or error, it must not be merged. Resolve all warnings before committing.
|
||||
2. **Type Safety:** Ensure interfaces in `src/lib/types/ae_types.ts` match backend schemas.
|
||||
3. **Reactivity Check:** Verify Svelte 5 runes (`$state`, `$derived`) are not creating race conditions with Dexie `liveQuery`.
|
||||
4. **Build Check:** For major changes, run `npm run build:staging` to ensure no SSR or build-time failures.
|
||||
4. **Build Check:** For major changes, run `npm run build:dev` to ensure no SSR or build-time failures.
|
||||
5. **Integration Tests:** For changes to badge print, event layouts, or auth/store logic, run the relevant Playwright test file(s):
|
||||
```bash
|
||||
npx playwright test tests/event_badge_render.test.ts tests/event_badge_attendee_workflow.test.ts
|
||||
|
||||
14
package.json
14
package.json
@@ -7,8 +7,9 @@
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"build:prod": "cp .env.prod .env.production && vite build",
|
||||
"build:staging": "cp .env.staging .env.production && vite build",
|
||||
"build:dev": "vite build --mode dev",
|
||||
"build:test": "vite build --mode test",
|
||||
"build:prod": "vite build --mode prod",
|
||||
"preview": "vite preview",
|
||||
"test": "npm run test:integration && npm run test:unit",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
@@ -17,9 +18,12 @@
|
||||
"format": "prettier --write .",
|
||||
"test:integration": "playwright test",
|
||||
"test:unit": "vitest",
|
||||
"deploy:staging": "docker compose -f ../aether_container_env/docker-compose.yml build ae_app && docker compose -f ../aether_container_env/docker-compose.yml up -d ae_app",
|
||||
"deploy:prod": "docker compose -f ../aether_container_env/docker-compose.yml build --build-arg BUILD_MODE=prod ae_app && docker compose -f ../aether_container_env/docker-compose.yml up -d --remove-orphans ae_app",
|
||||
"compose:down": "docker compose -f ../aether_container_env/docker-compose.yml --profile database down"
|
||||
"build:docker:dev": "docker compose -f ../aether_container_env/docker-compose.yml build ae_app && docker compose -f ../aether_container_env/docker-compose.yml up -d ae_app",
|
||||
"build:docker:test": "docker compose -f ../aether_container_env/docker-compose.yml build --build-arg BUILD_MODE=test ae_app && docker compose -f ../aether_container_env/docker-compose.yml up -d ae_app",
|
||||
"build:docker:prod": "docker compose -f ../aether_container_env/docker-compose.yml build --build-arg BUILD_MODE=prod ae_app && docker compose -f ../aether_container_env/docker-compose.yml up -d --remove-orphans ae_app",
|
||||
"compose:down": "docker compose -f ../aether_container_env/docker-compose.yml --profile database down",
|
||||
"deploy:remote:test": "ssh linode.oneskyit.com 'bash /srv/env/test_aether/deploy.sh test'",
|
||||
"deploy:remote:prod": "ssh linode.oneskyit.com 'bash /srv/env/prod_aether/deploy.sh prod'"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
|
||||
@@ -173,7 +173,13 @@ export const get_object = async function get_object({
|
||||
const fetchOptions: RequestInit = {
|
||||
method: 'GET',
|
||||
headers: headers_cleaned,
|
||||
signal: controller.signal
|
||||
signal: controller.signal,
|
||||
// Be explicit about CORS behavior and redirect handling to avoid
|
||||
// environment-dependent defaults that can cause opaque failures.
|
||||
mode: 'cors',
|
||||
credentials: 'omit',
|
||||
redirect: 'follow',
|
||||
cache: 'no-store'
|
||||
};
|
||||
|
||||
if (log_lvl > 1) {
|
||||
@@ -259,6 +265,14 @@ export const get_object = async function get_object({
|
||||
console.log(
|
||||
`Response: status=${response.status} statusText=${response.statusText} url=${response.url} attempt=${attempt}`
|
||||
);
|
||||
try {
|
||||
console.log(
|
||||
'Response headers:',
|
||||
Object.fromEntries(response.headers.entries())
|
||||
);
|
||||
} catch (e) {
|
||||
/* ignore header read errors */
|
||||
}
|
||||
}
|
||||
if (log_lvl > 1) {
|
||||
console.log('Response:', response);
|
||||
|
||||
@@ -147,7 +147,7 @@ function handle_clip_video(event: Event) {
|
||||
api_cfg: $ae_api,
|
||||
endpoint: endpoint,
|
||||
params: params,
|
||||
timeout: 300000, // 5 minutes
|
||||
timeout: 1800000, // 30 minutes — clip_video runs ffmpeg which can take 5-15 min for long recordings
|
||||
// return_blob: true,
|
||||
// filename: event.target.new_filename.value,
|
||||
// auto_download: false,
|
||||
@@ -157,12 +157,24 @@ function handle_clip_video(event: Event) {
|
||||
.then(function (result) {
|
||||
console.log(result);
|
||||
|
||||
// WHY: get_object() returns false/null on network failure (CORS drop,
|
||||
// timeout, Gunicorn worker killed). Without this guard, result.hosted_file_id
|
||||
// is undefined — JS uses it as an object key without throwing, so all the
|
||||
// success state below would run and show "Clipped" on a failed request.
|
||||
if (!result || !result.hosted_file_id) {
|
||||
console.log('clip_video: request failed or returned no result');
|
||||
$ae_sess.files.processed_file_kv[hosted_file_id].submit_status =
|
||||
'error';
|
||||
$ae_loc.files.processed_file_kv[hosted_file_id].submit_status =
|
||||
'error';
|
||||
submit_status = 'error';
|
||||
clip_complete = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
video_clip_file_kv[result.hosted_file_id] = {};
|
||||
video_clip_file_kv[result.hosted_file_id] = result;
|
||||
|
||||
// $ae_loc.files.video_clip_file_kv[result.hosted_file_id] = {};
|
||||
// $ae_loc.files.video_clip_file_kv[result.hosted_file_id] = result;
|
||||
|
||||
$ae_sess.files.processed_file_kv[hosted_file_id].submit_status =
|
||||
'clipped';
|
||||
$ae_sess.files.processed_file_kv[hosted_file_id].clip_complete =
|
||||
@@ -176,13 +188,6 @@ function handle_clip_video(event: Event) {
|
||||
submit_status = 'clipped';
|
||||
clip_complete = true;
|
||||
|
||||
// let file_blob = new Blob([result.data]);
|
||||
// // console.log(file_blob);
|
||||
// let file_obj_url = window.URL.createObjectURL(file_blob); // The img src
|
||||
// // const url = window.URL.createObjectURL(new Blob([result.data]));
|
||||
// download_clip_src = file_obj_url;
|
||||
// // download_filename = file_obj_url;
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -363,21 +368,25 @@ function handle_clip_video(event: Event) {
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-lg btn-primary preset-tonal-primary border-primary-500 hover:preset-filled-primary-500 border transition-colors"
|
||||
class="btn btn-lg btn-primary border transition-colors"
|
||||
class:preset-tonal-primary={submit_status !== 'error'}
|
||||
class:border-primary-500={submit_status !== 'error'}
|
||||
class:hover:preset-filled-primary-500={submit_status !== 'error'}
|
||||
class:preset-tonal-error={submit_status === 'error'}
|
||||
class:border-error-500={submit_status === 'error'}
|
||||
disabled={submit_status == 'clipping'}>
|
||||
<!-- {#await ae_promises[hosted_file_id]} -->
|
||||
{#if $ae_loc.files.processed_file_kv[hosted_file_id] && $ae_loc.files.processed_file_kv[hosted_file_id].submit_status == 'clipping'}
|
||||
<LoaderCircle size="1em" class="m-1 animate-spin" />
|
||||
<span class="highlight">Clipping...</span>
|
||||
{:else if $ae_loc.files.processed_file_kv[hosted_file_id] && $ae_loc.files.processed_file_kv[hosted_file_id].submit_status == 'error'}
|
||||
<span class="fas fa-exclamation-triangle m-1"></span>
|
||||
Failed — Retry?
|
||||
{:else if $ae_loc.files.processed_file_kv[hosted_file_id] && $ae_loc.files.processed_file_kv[hosted_file_id].submit_status == 'clipped'}
|
||||
<Check size="1em" class="m-1" />
|
||||
Clipped
|
||||
{:else}
|
||||
<!-- {#if ae_promises[hosted_file_id]} -->
|
||||
{#if $ae_loc.files.processed_file_kv[hosted_file_id] && $ae_loc.files.processed_file_kv[hosted_file_id].submit_status == 'clipped'}
|
||||
<Check size="1em" class="m-1" />
|
||||
Clipped
|
||||
{:else}
|
||||
<Scissors size="1em" class="m-1" />
|
||||
Clip Video
|
||||
{/if}
|
||||
<Scissors size="1em" class="m-1" />
|
||||
Clip Video
|
||||
{/if}
|
||||
<!-- <span class="fas fa-cut m-1"></span>
|
||||
Clip Video -->
|
||||
|
||||
@@ -18,10 +18,10 @@ import {
|
||||
PUBLIC_AE_API_SECRET_KEY,
|
||||
PUBLIC_AE_API_CRUD_SUPER_KEY,
|
||||
PUBLIC_AE_NO_ACCOUNT_ID,
|
||||
PUBLIC_AE_NO_ACCOUNT_ID_TOKEN,
|
||||
PUBLIC_AE_ACCOUNT_ID,
|
||||
PUBLIC_AE_EVENT_ID,
|
||||
PUBLIC_AE_SPONSORSHIP_CFG_ID
|
||||
// PUBLIC_AE_NO_ACCOUNT_ID_TOKEN,
|
||||
// PUBLIC_AE_ACCOUNT_ID,
|
||||
// PUBLIC_AE_EVENT_ID,
|
||||
// PUBLIC_AE_SPONSORSHIP_CFG_ID
|
||||
} from '$env/static/public';
|
||||
|
||||
const api_server_fqdn = PUBLIC_AE_API_SERVER; // 'api.oneskyit.com'
|
||||
@@ -34,9 +34,9 @@ const api_crud_super_key = PUBLIC_AE_API_CRUD_SUPER_KEY;
|
||||
// const ae_account_id = PUBLIC_AE_ACCOUNT_ID;
|
||||
const ae_account_id: null | string = null;
|
||||
const ae_no_account_id = PUBLIC_AE_NO_ACCOUNT_ID;
|
||||
const ae_no_account_id_token = PUBLIC_AE_NO_ACCOUNT_ID_TOKEN;
|
||||
const ae_event_id = PUBLIC_AE_EVENT_ID;
|
||||
const ae_sponsorship_cfg_id = PUBLIC_AE_SPONSORSHIP_CFG_ID;
|
||||
// const ae_no_account_id_token = PUBLIC_AE_NO_ACCOUNT_ID_TOKEN;
|
||||
// const ae_event_id = PUBLIC_AE_EVENT_ID;
|
||||
// const ae_sponsorship_cfg_id = PUBLIC_AE_SPONSORSHIP_CFG_ID;
|
||||
|
||||
// Loose bag-of-anything type used throughout the store. Once stores are broken into
|
||||
// typed domain files, individual structs will have proper interfaces instead.
|
||||
@@ -174,7 +174,7 @@ const ae_app_local_data_defaults: key_val = {
|
||||
posts: {},
|
||||
|
||||
sponsorships: {
|
||||
cfg_id: ae_sponsorship_cfg_id,
|
||||
cfg_id: null,
|
||||
|
||||
for_type: null,
|
||||
for_id: null,
|
||||
@@ -350,7 +350,7 @@ const slct_obj_template: key_val = {
|
||||
sponsorship_id: null,
|
||||
sponsorship_obj: {},
|
||||
sponsorship_obj_li: [],
|
||||
sponsorship_cfg_id: ae_sponsorship_cfg_id,
|
||||
sponsorship_cfg_id: null,
|
||||
sponsorship_cfg_obj: {},
|
||||
sponsorship_cfg_obj_li: [],
|
||||
post_id: null,
|
||||
|
||||
@@ -626,15 +626,14 @@ async function get_novi_group_moderators(
|
||||
const requestOptions = { method: 'GET', headers: headers };
|
||||
let allModeratorsRaw: any[] = [];
|
||||
|
||||
if (log_lvl) {
|
||||
console.log('Jitsi: Fetching Novi group moderators...');
|
||||
}
|
||||
console.log('[DEBUG] Novi Moderator Group Fetch:');
|
||||
console.log(' Group GUIDs:', group_guid_li);
|
||||
console.log(' Novi API Root URL:', api_root_url);
|
||||
|
||||
for (const group_guid of group_guid_li) {
|
||||
const url = `${api_root_url}/groups/${group_guid}/members?pageSize=200`;
|
||||
console.log(
|
||||
`Jitsi: Fetching moderator list for group ${group_guid} from: ${url}`
|
||||
);
|
||||
console.log(`[DEBUG] Fetching moderator list for group:`, group_guid);
|
||||
console.log(' Request URL:', url);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, requestOptions);
|
||||
@@ -647,42 +646,40 @@ async function get_novi_group_moderators(
|
||||
groupModList = result.Results;
|
||||
else if (Array.isArray(result.Members))
|
||||
groupModList = result.Members;
|
||||
else
|
||||
else {
|
||||
console.warn(
|
||||
`Jitsi: Moderator list format unexpected for group ${group_guid}.`,
|
||||
`[DEBUG] Moderator list format unexpected for group ${group_guid}. Raw result:`,
|
||||
result
|
||||
);
|
||||
}
|
||||
|
||||
allModeratorsRaw = allModeratorsRaw.concat(groupModList);
|
||||
if (log_lvl) {
|
||||
console.log(
|
||||
`Jitsi: Fetched ${groupModList.length} moderators from group ${group_guid}. Total: ${allModeratorsRaw.length}`
|
||||
);
|
||||
console.log(
|
||||
`[DEBUG] Group ${group_guid}: fetched ${groupModList.length} moderators. Total so far: ${allModeratorsRaw.length}`
|
||||
);
|
||||
if (groupModList.length > 0) {
|
||||
console.log(`[DEBUG] First 3 moderator objects:`, groupModList.slice(0, 3));
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`Jitsi: Failed to fetch moderators for group ${group_guid}. Status: ${response.status}`
|
||||
`[DEBUG] Failed to fetch moderators for group ${group_guid}. Status: ${response.status}`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Jitsi: Error fetching moderators for group ${group_guid}:`,
|
||||
`[DEBUG] Error fetching moderators for group ${group_guid}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (allModeratorsRaw.length === 0) {
|
||||
console.warn(
|
||||
'Jitsi: No moderators found across all specified Novi groups.'
|
||||
);
|
||||
console.warn('[DEBUG] No moderators found across all specified Novi groups.');
|
||||
}
|
||||
|
||||
if (log_lvl > 1) {
|
||||
console.log(
|
||||
'Jitsi: Fetched all raw moderators (combined):',
|
||||
allModeratorsRaw
|
||||
);
|
||||
console.log('[DEBUG] Combined moderator list count:', allModeratorsRaw.length);
|
||||
if (allModeratorsRaw.length > 0) {
|
||||
console.log('[DEBUG] Example moderator IDs:', allModeratorsRaw.slice(0, 5).map(m => m?.UniqueID || m?.UniqueId || m?.DuesPayerUniqueID || m?.id || ''));
|
||||
}
|
||||
|
||||
const modIdSet = new Set(
|
||||
@@ -698,6 +695,7 @@ async function get_novi_group_moderators(
|
||||
.filter(Boolean)
|
||||
.map((id: string) => String(id).toLowerCase().trim())
|
||||
);
|
||||
console.log('[DEBUG] Final moderator ID set:', Array.from(modIdSet));
|
||||
return modIdSet;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user