From 27fa280c192aedce3b6c1b77fd82bf0227246b3f Mon Sep 17 00:00:00 2001 From: PThorpe92 Date: Thu, 5 Sep 2024 23:35:08 -0400 Subject: [PATCH] fix: issue with consent page/oauth, seeder, retain build info --- Makefile | 6 +- README.md | 28 ++++++-- backend/migrations/main.go | 4 ++ backend/seeder/main.go | 36 +++++------ config/Dockerfile | 1 + config/docker-compose.kolibri.yml | 9 ++- config/hydra/hydra.yml | 1 + docker-compose.yml | 9 ++- frontend/src/Components/PageNav.tsx | 4 +- frontend/src/Components/forms/LoginForm.tsx | 36 ++++++++--- frontend/src/Pages/Auth/Consent.tsx | 8 +-- frontend/src/app.tsx | 71 ++++++++++++++++----- frontend/src/common.ts | 1 + frontend/vite.config.ts | 1 + provider-middleware/kolibri.go | 2 +- 15 files changed, 145 insertions(+), 72 deletions(-) diff --git a/Makefile b/Makefile index ece5618f..fff51d4a 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,3 @@ -INSTALL_GOOSE=go install github.com/pressly/goose/v3/cmd/goose@latest -INSTALL_AIR=go install github.com/air-verse/air@latest -INIT_HOOKS=cd frontend && yarn prepare && cd .. DOCKER_COMPOSE=docker-compose.yml KOLIBRI_COMPOSE=config/docker-compose.kolibri.yml MIGRATION_DIR=-dir backend/migrations @@ -43,8 +40,9 @@ reset: ascii_art init: ascii_art @echo 'Installing dependencies...' - $(INSTALL_GOOSE) && $(INIT_HOOKS) && $(INSTALL_AIR) + go install github.com/pressly/goose/v3/cmd/goose@latest && go install github.com/air-verse/air@latest && cd frontend && yarn install && yarn prepare && cd .. @echo 'Dependencies installed successfully.' + docker-compose up $(BUILD_RECREATE) dev: ascii_art docker-compose up $(BUILD_RECREATE) diff --git a/README.md b/README.md index 115c058f..6c60d6a1 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,14 @@ If you would like to contribute, please have a look at our [contribution guideli ### Dependencies (Local) +**Please ensure you have the following installed properly before the further steps** + - Go 1.23 -- Node.js > 18.0 + +- Node.js >= v18.0 + +- Yarn 1.22.22 + - Docker && Docker-Compose ### Dependencies (Deployment/Production) @@ -29,15 +35,22 @@ If you would like to contribute, please have a look at our [contribution guideli ### Steps -- Clone the repository +- Clone the repository. + +- `cp .env.example .env`. -- `cp .env.example .env` +- run `make init` to install dependencies, setup git hooks, and run the containers. -- run `make init` to install dependencies and setup git hooks +- run `make migrate` to run the initial migrations and populate database tables. -- run `make dev` to start the development environment in docker compose +- Optionally, If you wish to seed the database with some basic test data, run `make seed`. -NOTE: you must be sure to use `127.0.0.1` in place of `localhost`, as the cookies required for authentication are not shared between the two, + +Subsequent runs can be done with `make dev`, which will start all the necessary containers +with hot reloading on the client and the backend. + + +**NOTE:** you _must_ be sure to use `127.0.0.1` in place of `localhost` in your browser, as the cookies required for authentication are not shared between the two, and this can cause bad states in the browser that will prevent successful login/auth flow. Login with `SuperAdmin` and password: `ChangeMe!` @@ -45,6 +58,7 @@ Login with `SuperAdmin` and password: `ChangeMe!` You will be prompted immediately to set a new password and name for the default facility, and then you will be redirected to the dashboard. + **Integrations:** - _Kolibri_: @@ -63,7 +77,7 @@ will be redirected to the dashboard. ### To migrate the database to a fresh state, run `make migrate-fresh` (you can do this while docker is running with all the services, but you must restart the server (e.g. `docker restart unlockedv2-server-1` if your repo directory is called UnlockEdv2) -### To seed the database with some basic test data, run `make seed` + ### **Quick fixes to common issues with development** diff --git a/backend/migrations/main.go b/backend/migrations/main.go index 6b7b4cea..36b1b18f 100644 --- a/backend/migrations/main.go +++ b/backend/migrations/main.go @@ -46,6 +46,9 @@ func main() { if err := goose.Up(db, migrationDir); err != nil { log.Fatalf("Migration failed: %v", err) } + if *fresh { + MigrateFresh(db) + } log.Println("Migrations completed successfully") os.Exit(0) } @@ -95,6 +98,7 @@ func syncOryKratos() error { log.Fatal("unable to delete identity from Ory Kratos") continue } else { + log.Println("identity deleted successfully") continue } } diff --git a/backend/seeder/main.go b/backend/seeder/main.go index cfb705cd..9bff9409 100644 --- a/backend/seeder/main.go +++ b/backend/seeder/main.go @@ -61,6 +61,10 @@ func seedTestData(db *gorm.DB) { log.Printf("Failed to create platform: %v", err) } } + var newPlatforms []models.ProviderPlatform + if err := db.Find(&newPlatforms).Error; err != nil { + log.Fatal("Failed to get platforms from db") + } userFile, err := os.ReadFile("backend/tests/test_data/users.json") if err != nil { log.Printf("Failed to read test data: %v", err) @@ -69,23 +73,23 @@ func seedTestData(db *gorm.DB) { if err := json.Unmarshal(userFile, &users); err != nil { log.Printf("Failed to unmarshal test data: %v", err) } - for idx, u := range users { - u.Password = "ChangeMe!" - if err := u.HashPassword(); err != nil { + for idx := range users { + users[idx].Password = "ChangeMe!" + if err := users[idx].HashPassword(); err != nil { log.Fatalf("unable to hash user password") } - log.Printf("Creating user %s", u.Username) - if err := db.Create(&u).Error; err != nil { + log.Printf("Creating user %s", users[idx].Username) + if err := db.Create(&users[idx]).Error; err != nil { log.Printf("Failed to create user: %v", err) } - if err := testServer.HandleCreateUserKratos(u.Username, "ChangeMe!"); err != nil { + if err := testServer.HandleCreateUserKratos(users[idx].Username, "ChangeMe!"); err != nil { log.Fatalf("unable to create test user in kratos") } - for i := 0; i < len(platform); i++ { + for i := 0; i < len(newPlatforms); i++ { mapping := models.ProviderUserMapping{ - UserID: u.ID, - ProviderPlatformID: platform[i].ID, - ExternalUsername: u.Username, + UserID: users[idx].ID, + ProviderPlatformID: newPlatforms[i].ID, + ExternalUsername: users[idx].Username, ExternalUserID: strconv.Itoa(idx), } if err = db.Create(&mapping).Error; err != nil { @@ -101,19 +105,11 @@ func seedTestData(db *gorm.DB) { if err := json.Unmarshal(progs, &programs); err != nil { log.Printf("Failed to unmarshal test data: %v", err) } - for _, p := range programs { - if err := db.Create(&p).Error; err != nil { + for idx := range programs { + if err := db.Create(&programs[idx]).Error; err != nil { log.Printf("Failed to create program: %v", err) } } - var milestones []models.Milestone - mstones, err := os.ReadFile("backend/tests/test_data/milestones.json") - if err != nil { - log.Printf("Failed to read test data: %v", err) - } - if err := json.Unmarshal(mstones, &milestones); err != nil { - log.Printf("Failed to unmarshal test data: %v", err) - } outcomes := []string{"college_credit", "grade", "certificate", "pathway_completion"} milestoneTypes := []models.MilestoneType{models.DiscussionPost, models.AssignmentSubmission, models.QuizSubmission, models.GradeReceived} var dbUsers []models.User diff --git a/config/Dockerfile b/config/Dockerfile index e2052abc..7b64ddd7 100644 --- a/config/Dockerfile +++ b/config/Dockerfile @@ -2,4 +2,5 @@ FROM node:22.7.0-slim WORKDIR /app EXPOSE 5173 +RUN yarn install ENTRYPOINT ["yarn", "dev"] diff --git a/config/docker-compose.kolibri.yml b/config/docker-compose.kolibri.yml index 840f7a14..9e94b25e 100644 --- a/config/docker-compose.kolibri.yml +++ b/config/docker-compose.kolibri.yml @@ -3,8 +3,13 @@ services: kolibri: image: unlockedlabs.org/kolibri:latest environment: - CLIENT_ID: #placeholder for generated - CLIENT_SECRET: #client on first login + CLIENT_ID: # replace with generated client ID + CLIENT_SECRET: # replace with generated client secret + KOLIBRI_OIDC_JWKS_URI: http://hydra:4444/.well-known/jwks.json + KOLIBRI_OIDC_AUTHORIZATION_ENDPOINT: http://hydra:4444/oauth2/auth + KOLIBRI_OIDC_TOKEN_ENDPOINT: http://hydra:4444/oauth2/token + KOLIBRI_OIDC_USERINFO_ENDPOINT: http://hydra:4444/userinfo + KOLIBRI_OIDC_CLIENT_URL: http://127.0.0.1:8000 ports: - 8000:8000 networks: diff --git a/config/hydra/hydra.yml b/config/hydra/hydra.yml index 5b4e3904..ddca0132 100644 --- a/config/hydra/hydra.yml +++ b/config/hydra/hydra.yml @@ -58,5 +58,6 @@ oidc: subject_identifiers: supported_types: - pairwise + - public pairwise: salt: 2839o82hy2839OO#@#$@OFw@ksj8*^@*^$LSwsifw2692oCHANGE_ME_IN_PROD diff --git a/docker-compose.yml b/docker-compose.yml index 6cf72b6d..36dd1864 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,7 +26,7 @@ services: - ./frontend:/app networks: - intranet - restart: always + restart: on-failure rev_proxy: image: nginx:1.21.3-alpine @@ -37,7 +37,7 @@ services: - logs:/var/log/nginx/ networks: - intranet - restart: "on-failure" + restart: on-failure depends_on: kratos-migrate: condition: service_completed_successfully @@ -103,6 +103,7 @@ services: - intranet volumes: - logs:/logs + restart: on-failure depends_on: postgres: condition: service_healthy @@ -175,7 +176,6 @@ services: - keto-migrate environment: - DSN=postgres://keto:ChangeMe!@postgres:5432/accesscontroldb?sslmode=prefer&max_conns=20&max_idle_conns=4 - restart: on-failure networks: - intranet @@ -188,7 +188,6 @@ services: source: ./config/kratos target: /etc/config/kratos command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes - restart: on-failure networks: - intranet depends_on: @@ -233,7 +232,7 @@ services: - SERVE_ADMIN_CORS_ENABLED=true - SERVE_ADMIN_CORS_ALLOWED_METHODS=POST,GET,PUT,DELETE - DSN=postgres://hydra:ChangeMe!@postgres:5432/hydra?sslmode=prefer&max_conns=20&max_idle_conns=4 - restart: unless-stopped + restart: on-failure depends_on: - hydra-migrate networks: diff --git a/frontend/src/Components/PageNav.tsx b/frontend/src/Components/PageNav.tsx index 89a375ab..0d1488d4 100644 --- a/frontend/src/Components/PageNav.tsx +++ b/frontend/src/Components/PageNav.tsx @@ -61,9 +61,7 @@ export default function PageNav({ )} - {path.map((p) => ( -
  • {p}
  • - ))} + {path && path.map((p) =>
  • {p}
  • )}
    diff --git a/frontend/src/Components/forms/LoginForm.tsx b/frontend/src/Components/forms/LoginForm.tsx index 4eb1f27a..24a113c2 100644 --- a/frontend/src/Components/forms/LoginForm.tsx +++ b/frontend/src/Components/forms/LoginForm.tsx @@ -92,17 +92,33 @@ export default function LoginForm() { window.location.replace(BROWSER_URL); return; } - let url = kratosUrl + queryParams.get('flow'); - const resp = await axios.get(url); - if (resp.status !== 200) { - console.error('Error initializing login flow'); - return; + const flowId = queryParams.get('flow'); + let url = kratosUrl + flowId; + + try { + const resp = await axios.get(url); + if (resp.status !== 200 || !resp.data) { + console.error( + 'Error initializing login flow or response data is missing' + ); + return; + } + if ( + !resp.data.id || + !resp.data.oauth2_login_challenge || + !resp.data.ui?.nodes[0]?.attributes?.value + ) { + console.error('Required fields from flow are missing'); + return; + } + return { + flow_id: resp.data.id, + challenge: resp.data.oauth2_login_challenge, + csrf_token: resp.data.ui.nodes[0].attributes.value + }; + } catch (error) { + console.error('Failed to fetch flow data', error); } - return { - flow_id: resp.data.id, - challenge: resp.data.oauth2_login_challenge, - csrf_token: resp.data.ui.nodes[0].attributes.value - }; }; return ( diff --git a/frontend/src/Pages/Auth/Consent.tsx b/frontend/src/Pages/Auth/Consent.tsx index 71d0e246..55bf58b6 100644 --- a/frontend/src/Pages/Auth/Consent.tsx +++ b/frontend/src/Pages/Auth/Consent.tsx @@ -3,10 +3,8 @@ import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; export default function Consent() { return ( -
    - - - -
    + + + ); } diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index 6c8a2c03..948bf29c 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -43,54 +43,92 @@ export default function App() { }, { path: '/dashboard', - element: WithAuth({ children: }), + element: ( + + + + ), errorElement: }, { path: '/users', - element: WithAdmin({ children: }), + element: ( + + + + ), errorElement: }, { path: '/resources-management', - element: WithAdmin({ children: }), + element: ( + + + + ), errorElement: }, { path: '/reset-password', - element: WithAuth({ children: }), + element: ( + + + + ), errorElement: }, { path: '/consent', - element: WithAuth({ - children: - }), + element: ( + + + + ), errorElement: }, { path: '/provider-platform-management', - element: WithAdmin({ children: }), + element: ( + + + + ), errorElement: }, { path: '/my-courses', - element: WithAuth({ children: }), + element: ( + + + + ), errorElement: }, { path: '/my-progress', - element: WithAuth({ children: }), + element: ( + + + + ), errorElement: }, { path: '/course-catalog', - element: WithAuth({ children: }), + element: ( + + + + ), errorElement: }, { path: '/provider-users/:providerId', - element: WithAdmin({ children: }), + element: ( + + + + ), errorElement: }, { @@ -99,9 +137,12 @@ export default function App() { }, { path: '/*', - element: WithAuth({ - children: - }) + element: ( + + + + ), + errorElement: } ]); diff --git a/frontend/src/common.ts b/frontend/src/common.ts index 10b19f06..77a99270 100644 --- a/frontend/src/common.ts +++ b/frontend/src/common.ts @@ -108,6 +108,7 @@ export interface Program { created_at: Date; updated_at: Date; } + export interface CourseCatalogue { key: [number, string, boolean]; program_id: number; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index afcdb09e..35942c03 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -5,6 +5,7 @@ import path from 'path'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + build: { sourcemap: true }, // we keep the sourcemap for now, so we can debug in staging resolve: { alias: { '@': path.resolve(__dirname, 'src') diff --git a/provider-middleware/kolibri.go b/provider-middleware/kolibri.go index edc8a4eb..e5ae0970 100644 --- a/provider-middleware/kolibri.go +++ b/provider-middleware/kolibri.go @@ -135,7 +135,7 @@ type KolibriActivity struct { } func (ks *KolibriService) ImportActivityForProgram(programIdPair map[string]interface{}, db *gorm.DB) error { - programId := int(programIdPair["id"].(float64)) + programId := int(programIdPair["program_id"].(float64)) externalId := programIdPair["external_id"].(string) sql := `SELECT id, user_id, time_spent, completion_timestamp, content_id, progress, kind FROM logger_contentsummarylog WHERE channel_id = ?` var activities []KolibriActivity