diff --git a/.env.prod.example b/.env.prod.example index 54645cb..a5a4260 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -19,3 +19,51 @@ DJANGO_SUPERUSER_EMAIL= CHANGE THIS TO A VALID EMAIL DJANGO_SUPERUSER_USERNAME= CHANGE THIS TO A VALID USERNAME FRONTEND_URL="https://xxxx.codersforcauses.org/" + +# KEYCLOAK +KEYCLOAK_HTTP_PORT=8080 +KEYCLOAK_MANAGEMENT_PORT=9000 +KEYCLOAK_IMPORT_REALM_ON_STARTUP=false + +KC_BOOTSTRAP_ADMIN_USERNAME=admin +KC_BOOTSTRAP_ADMIN_PASSWORD= CHANGE THIS TO A RANDOM STRING +KC_DB=postgres +KC_DB_URL=jdbc:postgresql://db:5432/postgres +KC_DB_USERNAME=postgres +KC_DB_PASSWORD=password +KC_HOSTNAME=auth.example.com +KC_HTTP_ENABLED=true +KC_PROXY_HEADERS=xforwarded +KC_HEALTH_ENABLED=true +KC_METRICS_ENABLED=true + +KEYCLOAK_BASE_URL=https://auth.example.com +KEYCLOAK_REALM=agronomy-club +KEYCLOAK_ISSUER=https://auth.example.com/realms/agronomy-club +KEYCLOAK_JWKS_URL=https://auth.example.com/realms/agronomy-club/protocol/openid-connect/certs +KEYCLOAK_AUTH_URL=https://auth.example.com/realms/agronomy-club/protocol/openid-connect/auth +KEYCLOAK_TOKEN_URL=https://auth.example.com/realms/agronomy-club/protocol/openid-connect/token +KEYCLOAK_USERINFO_URL=https://auth.example.com/realms/agronomy-club/protocol/openid-connect/userinfo + +DJANGO_OIDC_CLIENT_ID=django-admin +DJANGO_OIDC_CLIENT_SECRET= CHANGE THIS TO A RANDOM STRING +FIRST_PLATFORM_ADMIN_USERNAME=admin +FIRST_PLATFORM_ADMIN_EMAIL=admin@example.com +FIRST_PLATFORM_ADMIN_PASSWORD= CHANGE THIS TO A RANDOM STRING +FIRST_DJANGO_ADMIN_USERNAME=admin +FIRST_DJANGO_ADMIN_EMAIL=admin@example.com +FIRST_DJANGO_ADMIN_PASSWORD= CHANGE THIS TO A RANDOM STRING +FIRST_KEYCLOAK_ADMIN_USERNAME=admin +FIRST_KEYCLOAK_ADMIN_PASSWORD= CHANGE THIS TO A RANDOM STRING +KEYCLOAK_SYNC_EFFECTIVE_ROLES=true +KEYCLOAK_ADMIN_REALM=master +KEYCLOAK_ADMIN_CLIENT_ID=admin-cli +KEYCLOAK_ADMIN_CLIENT_SECRET= +KEYCLOAK_ADMIN_USERNAME=admin +KEYCLOAK_ADMIN_PASSWORD= CHANGE THIS TO A RANDOM STRING +KEYCLOAK_TEST_ADMIN_EMAIL=admin@example.com +KEYCLOAK_TEST_ADMIN_USERNAME=admin +KEYCLOAK_TEST_ADMIN_PASSWORD= CHANGE THIS TO A RANDOM STRING +NEXT_PUBLIC_KEYCLOAK_URL=https://auth.example.com +NEXT_PUBLIC_KEYCLOAK_REALM=agronomy-club +NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=agronomy-web diff --git a/README.md b/README.md index 92a3814..b0a3753 100644 --- a/README.md +++ b/README.md @@ -199,8 +199,140 @@ python manage.py migrate # apply migrations If you run into migration conflicts that you can't be bothered to fix, run `nuke.sh` to clear your database. Then, run migrations again. +### User Self-Service Auth API + +The backend provides frontend-facing endpoints for normal user signup and login. + +Base URL (dev): `http://localhost:8000/api` + +1. Signup + +Endpoint: +`POST /auth/signup/` + +Request body: +```json +{ + "full_name": "Jane Example", + "grad_yr": 2031, + "discipline": "Agronomy", + "email": "jane@example.com", + "password": "StrongPass#2026" +} +``` + +Success response (`201 Created`): +```json +{ + "message": "Signup successful.", + "user": { + "id": 1, + "full_name": "Jane Example", + "grad_yr": 2031, + "discipline": "Agronomy", + "email": "jane@example.com", + "global_role": "user" + } +} +``` + +Common error responses: +- `400 Bad Request` for invalid payload, weak/short password, duplicate email, or invalid graduation year. + +2. Login + +Endpoint: +`POST /auth/login/` + +Request body: +```json +{ + "email": "jane@example.com", + "password": "StrongPass#2026" +} +``` + +Success response (`200 OK`): +```json +{ + "message": "Login successful.", + "user": { + "id": 1, + "full_name": "Jane Example", + "grad_yr": 2031, + "discipline": "Agronomy", + "email": "jane@example.com", + "global_role": "user" + } +} +``` + +Common error responses: +- `401 Unauthorized` when email/password is incorrect. +- `400 Bad Request` for malformed payload. + +Notes: +- These endpoints are intended for normal users (not Django admin/Keycloak admin). +- Passwords are stored as hashes in backend storage. + ## Other +### Keycloak Deployment + +This repository now includes a Keycloak deployment path and a realm skeleton. + +1. Bootstrap environment variables (creates missing values and secrets): +```bash +./scripts/bootstrap-keycloak-env.sh ./.env +``` + +2. Start dev docker services (db + keycloak): +```bash +docker compose -f docker-compose.yml up -d +``` + +3. One-command helper (bootstraps env, copies realm skeleton into import folder, starts compose): +```bash +./scripts/deploy-with-keycloak.sh +``` + +First login credentials are unified as one account for both Keycloak and Django admin (set these in root `.env`): + +- `FIRST_PLATFORM_ADMIN_USERNAME` +- `FIRST_PLATFORM_ADMIN_EMAIL` +- `FIRST_PLATFORM_ADMIN_PASSWORD` + +This same account is used for: +- Keycloak admin panel (`http://localhost:8080/admin`) +- Django admin panel with Keycloak auth (`http://localhost:8000/admin`) + +These are propagated by `./scripts/bootstrap-keycloak-env.sh` into: +- `KC_BOOTSTRAP_ADMIN_*` and `KEYCLOAK_ADMIN_*` for Keycloak admin API/auth +- `KEYCLOAK_TEST_ADMIN_*` (and `DJANGO_SUPERUSER_*`) for Django admin test/bootstrap login +- `FIRST_KEYCLOAK_ADMIN_*` and `FIRST_DJANGO_ADMIN_*` as synchronized aliases + +Legacy variables `FIRST_KEYCLOAK_ADMIN_*` and `FIRST_DJANGO_ADMIN_*` are still supported as fallbacks for backwards compatibility. + +To force an already-running Keycloak instance to use your configured first admin password: +```bash +./scripts/sync-keycloak-first-admin-password.sh ./.env +``` + +`./scripts/deploy-with-keycloak.sh` runs this sync automatically after Keycloak starts. + +4. Production helper: +```bash +./scripts/deploy-with-keycloak.sh --prod +``` + +Realm skeleton source: +- `keycloak/agronomy-club-realm-skeleton.json` + +Realm import path used by docker compose: +- `keycloak/import/agronomy-club-realm-skeleton.json` + +Note: chapter access scope groups are templated with `__CHAPTER_ID__` and should be generated for real chapter IDs. + ### Update Dependencies You can run `npm install` and `poetry install` in the respective `client` and `server` folders to install the newest dependencies. diff --git a/client/.env.example b/client/.env.example index 63e7f21..7a79d7c 100644 --- a/client/.env.example +++ b/client/.env.example @@ -1,4 +1,7 @@ REACT_EDITOR=code APP_ENV=DEVELOPMENT -NEXT_PUBLIC_BACKEND_URL="http://localhost:8000/api" \ No newline at end of file +NEXT_PUBLIC_BACKEND_URL="http://localhost:8000/api" +NEXT_PUBLIC_KEYCLOAK_URL="http://localhost:8080" +NEXT_PUBLIC_KEYCLOAK_REALM="agronomy-club" +NEXT_PUBLIC_KEYCLOAK_CLIENT_ID="agronomy-web" \ No newline at end of file diff --git a/client/next.config.mjs b/client/next.config.mjs index e0d81e7..e9441a6 100644 --- a/client/next.config.mjs +++ b/client/next.config.mjs @@ -18,15 +18,6 @@ const nextConfig = { return config; } : undefined, - - async rewrites() { - return [ - { - source: "/quizzes/download/:id", - destination: `${process.env.NEXT_PUBLIC_BACKEND_URL}/quizzes/download/:id/`, - }, - ]; - }, }; export default nextConfig; diff --git a/client/src/app/about/page.tsx b/client/src/app/about/page.tsx index d200c73..b712ef4 100644 --- a/client/src/app/about/page.tsx +++ b/client/src/app/about/page.tsx @@ -1,176 +1,50 @@ -import { Bug, Droplet, Mail, Sprout, Wheat } from "lucide-react"; import type { Metadata } from "next"; - -import { CommitteeMemberCard } from "@/components/committee-member-card"; +import Link from "next/link"; export const metadata: Metadata = { title: "About | Agronomy Club", description: "Discover the purpose, values, and impact strategy guiding the Agronomy Club.", }; -const committeeMembers = [ - { name: "Member 1", position: "President", photo: "" }, - { name: "Member 2", position: "Vice-President", photo: "" }, - { name: "Member 3", position: "Secretary", photo: "" }, - { name: "Member 4", position: "Treasurer", photo: "" }, - { name: "Member 5", position: "OCM", photo: "" }, -]; + export default function AboutPage() { return ( -
+
-
-

+

+

Our purpose

-

+

Growing resilient food systems through collaboration

-

- The Agronomy Club is a student-led community supporting excellence - in agronomic sciences. Through study tours, competitions, coaching, - case study discussions, study groups, learning resources, and - hands-on practice, the club helps members build practical knowledge - and professional connections. Originally founded in UWA, we aim to - grow this community to connect all the green thumbs across the - world. +

+ The Agronomy Club empowers students, researchers, and professionals + to collaborate on solutions that strengthen global food security. We + blend scientific exploration with community-driven field work to + accelerate sustainable agricultural impact.

- -
-

- Crop Adviser Competencies -

-

- These competencies support four main pillars of agronomic sciences - through practical learning, field-based activities, and student-led - collaboration. -

- -
-
-
-
-

- Develop practical knowledge of cropping systems, variety - selection, crop establishment, crop growth, technology use, - harvest, storage, and production economics. -

-
- -
-
-
-

- Explore plant nutrition, soil fertility, soil testing, nutrient - sources, placement, timing, diagnostics, and nutrient management - planning. -

-
- -
-
-
-

- Learn how soil health, water use, and irrigation practices shape - sustainable field management. -

-
- -
-
-
-

- Build skills in pest identification, sampling, monitoring, - decision-making, management strategies, environmental - stewardship, and health and safety. -

-
-
-
- -
-

- Current Agronomy Club Committee -

-

- Meet the current committee supporting the club, its members, and - student-led activities. -

-
- {committeeMembers.map((member) => ( - - ))} -
-
- -
-
-

- CONTACT -

-

- Contact the Club -

-

- Interested in the Agronomy Club or looking to connect with the - committee? Reach out to learn more about activities, chapters, - resources, and opportunities to get involved. -

-
- -
-
-
-

- - agronomy-club@uwa.edu.au - -

-
+
+

Our Pillars

+
    +
  • + ⚙️ Research & Innovation — Translating agronomic science into + applied solutions. +
  • +
  • + 🤝 Community Engagement — Connecting chapters and partners across + regions. +
  • +
  • + 🌱 Sustainability — Championing climate-smart practices and + regenerative agriculture. +
  • +
  • + 🎓 Education — Building leadership and technical skills in the + agri-food workforce. +
  • +
diff --git a/client/src/app/chapters/[id]/page.tsx b/client/src/app/chapters/[id]/page.tsx index df5cb39..85caea3 100644 --- a/client/src/app/chapters/[id]/page.tsx +++ b/client/src/app/chapters/[id]/page.tsx @@ -1,162 +1,3 @@ -import { Mail, MapPin } from "lucide-react"; -import Link from "next/link"; -import { notFound } from "next/navigation"; -import type { ReactNode } from "react"; - -import { CommitteeMemberCard } from "@/components/committee-member-card"; -import { Button } from "@/components/ui/button"; - -type CommitteeMember = { name: string; position: string; photo: string }; -type Chapter = { - id: string; - abbreviation: string; - name: string; - description: string; - bannerColour: string; - location: string; - contactEmail: string; - committee: CommitteeMember[]; -}; - -// ── Temporary mock data ── -const MOCK_CHAPTERS: Record = { - "1": { - id: "1", - abbreviation: "UWA AG", - name: "UWA Agronomy Club", - description: - "A student-led group helping students feel prepared for the agricultural industry through learning resources, events, and opportunities to connect with peers and alumni across the agronomy community. This paragraph is intentionally a little long so you can check how the text wraps and how the section stretches at different widths.", - bannerColour: "#3F7D27", - location: "Perth, Western Australia", - contactEmail: "hello@agronomyclub.org", - committee: [ - { name: "Ben Jerry", position: "President", photo: "" }, - { name: "Lily Chen", position: "Vice-President", photo: "" }, - { name: "David Smith", position: "Treaserer", photo: "" }, - { name: "David Kith", position: "Treasurer", photo: "" }, - ], - }, -}; - -async function getChapter(id: string): Promise { - return MOCK_CHAPTERS[id] ?? null; - // Real version later: - // const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/chapters/${id}/`, { cache: "no-store" }); - // if (!res.ok) return null; return res.json(); -} - -function SectionHeading({ - children, - colour, -}: { - children: ReactNode; - colour: string; -}) { - return ( -
-

{children}

-
-
- ); -} - -export default async function ChapterPage({ - params, -}: { - params: Promise<{ id: string }>; -}) { - const { id } = await params; - const chapter = await getChapter(id); - if (!chapter) notFound(); - - // Initials for the logo placeholder - const initials = chapter.name - .split(" ") - .map((word) => word[0]) - .slice(0, 2) - .join("") - .toUpperCase(); - - return ( -
- - ← All chapters - - - {/* Responsive */} -
- {/* identity card + reserved join button */} -
-
- {/* coloured banner strip */} -
- -
-
- {initials} -
-

- {chapter.abbreviation} -

-

- {chapter.name} -

-

- {" "} - {chapter.location} -

- - - {chapter.contactEmail} - -
-
- - {/* Reserved for the join button */} - -
- - {/* about + committee */} -
-
- About -

- {chapter.description} -

-
- -
- - Committee - - {/* committee-member-card component */} -
- {chapter.committee.map((member) => ( - - ))} -
-
-
-
-
- ); +export default function IndividualChapterPage() { + return <>Placeholder; } diff --git a/client/src/app/layout.tsx b/client/src/app/layout.tsx index c251720..50a60bc 100644 --- a/client/src/app/layout.tsx +++ b/client/src/app/layout.tsx @@ -4,7 +4,6 @@ import type { Metadata } from "next"; import { Merriweather, Roboto_Slab } from "next/font/google"; import Providers from "@/components/main/providers"; -import { Footer } from "@/components/ui/footer"; import Navbar from "@/components/ui/navbar"; export const metadata: Metadata = { @@ -36,7 +35,6 @@ export default function RootLayout({ {children} -