{"id":35211846,"url":"https://github.com/fhswf/appointme","last_synced_at":"2026-04-22T10:01:01.959Z","repository":{"id":38173560,"uuid":"308320898","full_name":"fhswf/appointme","owner":"fhswf","description":"Self service booking of consultation hours","archived":false,"fork":false,"pushed_at":"2026-04-21T11:31:25.000Z","size":20940,"stargazers_count":2,"open_issues_count":22,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-21T11:32:44.935Z","etag":null,"topics":["appointment","consultation-hours"],"latest_commit_sha":null,"homepage":"https://appointme.gawron.cloud","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fhswf.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2020-10-29T12:26:54.000Z","updated_at":"2026-04-21T11:31:29.000Z","dependencies_parsed_at":"2026-01-16T08:08:18.725Z","dependency_job_id":null,"html_url":"https://github.com/fhswf/appointme","commit_stats":{"total_commits":196,"total_committers":3,"mean_commits":65.33333333333333,"dds":0.173469387755102,"last_synced_commit":"4204ab9f87873b11a7ded52a23903a84f7c4ac05"},"previous_names":["fhswf/appointme","fhswf/book_me"],"tags_count":430,"template":false,"template_full_name":null,"purl":"pkg:github/fhswf/appointme","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fhswf%2Fappointme","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fhswf%2Fappointme/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fhswf%2Fappointme/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fhswf%2Fappointme/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fhswf","download_url":"https://codeload.github.com/fhswf/appointme/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fhswf%2Fappointme/sbom","scorecard":{"id":398719,"data":{"date":"2025-08-11","repo":{"name":"github.com/fhswf/book_me","commit":"7d54457034a658351840ce08c5d13acd9e16924c"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.4,"checks":[{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Code-Review","score":0,"reason":"Found 0/7 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/deploy.yml:1","Info: topLevel 'pull-requests' permission set to 'read': .github/workflows/quality.yml:37","Warn: no topLevel permission defined: .github/workflows/release.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE.md:0","Info: FSF or OSI recognized license: MIT License: LICENSE.md:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/quality.yml:46: update your workflow using https://app.stepsecurity.io/secureworkflow/fhswf/book_me/quality.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/quality.yml:75: update your workflow using https://app.stepsecurity.io/secureworkflow/fhswf/book_me/quality.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/quality.yml:85: update your workflow using https://app.stepsecurity.io/secureworkflow/fhswf/book_me/quality.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:20: update your workflow using https://app.stepsecurity.io/secureworkflow/fhswf/book_me/release.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/release.yml:23: update your workflow using https://app.stepsecurity.io/secureworkflow/fhswf/book_me/release.yml/main?enable=pin","Warn: containerImage not pinned by hash: .devcontainer/Dockerfile:1: pin your Docker image by updating mcr.microsoft.com/devcontainers/javascript-node:0-18 to mcr.microsoft.com/devcontainers/javascript-node:0-18@sha256:ed57dd8755b4e75a0426bd10ab1d3a60a22bb21fc2e093801375990978c42fb5","Warn: containerImage not pinned by hash: backend/Dockerfile:1","Warn: containerImage not pinned by hash: backend/Dockerfile:31","Warn: containerImage not pinned by hash: client/Dockerfile:1","Warn: containerImage not pinned by hash: client/Dockerfile:29: pin your Docker image by updating nginx:1.21.3-alpine to nginx:1.21.3-alpine@sha256:1ff1364a1c4332341fc0a854820f1d50e90e11bb0b93eb53b47dc5e10c680116","Info:   0 out of   4 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   1 third-party GitHubAction dependencies pinned","Info:   0 out of   5 containerImage dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"SAST","score":10,"reason":"SAST tool is run on all commits","details":["Info: all commits (25) are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":0,"reason":"38 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-968p-4wvh-cqc8","Warn: Project is vulnerable to: GHSA-xffm-g5w8-qvg7","Warn: Project is vulnerable to: GHSA-x4c5-c7rf-jjgv","Warn: Project is vulnerable to: GHSA-h5c3-5r3r-rr8q","Warn: Project is vulnerable to: GHSA-rmvr-2pp2-xj38","Warn: Project is vulnerable to: GHSA-xx4v-prfh-6cgc","Warn: Project is vulnerable to: GHSA-593m-55hh-j8gv","Warn: Project is vulnerable to: GHSA-wf5p-g6vw-rhxx","Warn: Project is vulnerable to: GHSA-jr5f-v2jv-69x6","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-pxg6-pf52-xh8x","Warn: Project is vulnerable to: GHSA-67mh-4wv8-2f99","Warn: Project is vulnerable to: GHSA-jchw-25xp-jwwc","Warn: Project is vulnerable to: GHSA-cxjh-pqwp-8mfp","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-75v8-2h7p-7m2m","Warn: Project is vulnerable to: GHSA-78xj-cgh5-2h22","Warn: Project is vulnerable to: GHSA-2p57-rm9w-gvfp","Warn: Project is vulnerable to: GHSA-35jh-r3h4-6jhm","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-m7xq-9374-9rvx","Warn: Project is vulnerable to: GHSA-vg7j-7cwx-8wgw","Warn: Project is vulnerable to: GHSA-qrpm-p2h7-hrv2","Warn: Project is vulnerable to: GHSA-mwcw-c2x4-8c55","Warn: Project is vulnerable to: GHSA-rhx6-c78j-4q9w","Warn: Project is vulnerable to: GHSA-mxhp-79qh-mcx6","Warn: Project is vulnerable to: GHSA-pq67-2wwv-3xjx","Warn: Project is vulnerable to: GHSA-8cj5-5rvv-wf4v","Warn: Project is vulnerable to: GHSA-52f5-9888-hmc6","Warn: Project is vulnerable to: GHSA-c76h-2ccp-4975","Warn: Project is vulnerable to: GHSA-cxrh-j4jr-qwg3","Warn: Project is vulnerable to: GHSA-vg6x-rcgg-rjx6","Warn: Project is vulnerable to: GHSA-x574-m823-4x7w","Warn: Project is vulnerable to: GHSA-4r4m-qw57-chr8","Warn: Project is vulnerable to: GHSA-xcj6-pq6g-qj4x","Warn: Project is vulnerable to: GHSA-356w-63v5-8wf4","Warn: Project is vulnerable to: GHSA-859w-5945-r5v3","Warn: Project is vulnerable to: GHSA-9crc-q9x8-hgqq"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-18T19:38:24.532Z","repository_id":38173560,"created_at":"2025-08-18T19:38:24.532Z","updated_at":"2025-08-18T19:38:24.532Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32130776,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-22T08:34:57.708Z","status":"ssl_error","status_checked_at":"2026-04-22T08:34:55.583Z","response_time":58,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["appointment","consultation-hours"],"created_at":"2025-12-29T19:01:28.644Z","updated_at":"2026-04-22T10:01:01.904Z","avatar_url":"https://github.com/fhswf.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)\n![GitHub issues](https://img.shields.io/github/issues/fhswf/appointme)\n![GitHub pull request check state](https://img.shields.io/github/status/s/pulls/fhswf/appointme/10)\n[![Quality Gate Status](https://sonarqube.fh-swf.cloud/api/project_badges/measure?project=fhswf_appointme_4a6870c1-a6b0-4818-9a4f-c9256685c9d3\u0026metric=alert_status\u0026token=sqb_5deb3d670ea0a882f6563a07b9bea9dee035bcb6)](https://sonarqube.fh-swf.cloud/dashboard?id=fhswf_appointme_4a6870c1-a6b0-4818-9a4f-c9256685c9d3)\n\n# APPointment\n\nThis web application helps you planning your appointments.\n\nAs a _provider_ of appointments (i.e. consultation hours) you can manage times when you are available for different types of appointments\n(online, in person, different durations) and integrate your Google calendar.\n\nAs a _client_, you can search for available slots and book an appointment. You will receive an invitation from the calendar service of the provider.\n\nFull documentation is available at [https://fhswf.github.io/appointme/](https://fhswf.github.io/appointme/).\n\n## Deployment\n\n### Deployment on Kubernetes\n\nTo deploy the application on Kubernetes, you need to create the necessary ConfigMap and Secret resources.\n\n1.  **Prepare Configuration:**\n    Detailed configuration templates are provided in `backend/k8s/`.\n    *   `backend/k8s/configmap.yaml.example`: Use this as a template. Rename it to `configmap.yaml` (or create a new one). **This is the central configuration for both backend and client.**\n        *   Updates to `API_URL` and `BASE_URL` here will configure the Backend.\n        *   Updates to `REACT_APP_API_URL` and `REACT_APP_URL` here will be injected into the Client.\n        *   Set `MONGO_URI` and `CORS_ALLOWED_ORIGINS` as needed.\n    *   `backend/k8s/secret.yaml.example`: Use this as a template. Rename it to `secret.yaml` (or create a new one) and set sensitive secrets. **Important:** Replace the placeholder values (e.g., `changeme`) with your actual secrets before applying.\n\n2.  **Apply Resources:**\n    ```bash\n    # Example command (after creating the actual files)\n    kubectl apply -f backend/k8s/configmap.yaml\n    kubectl apply -f backend/k8s/secret.yaml\n    ```\n\n3.  **Deploy Application:**\n    ```bash\n    kubectl apply -f backend/k8s/deployment.yaml\n    ```\n\n4.  **Deploy MCP Server:**\n    The MCP server is a separate deployment that integrates with the backend.\n    \n    ```bash\n    kubectl apply -k mcp-server/k8s/base\n    ```\n    \n    The Ingress handles routing:\n    *   `/` -\u003e Client\n    *   `/api` -\u003e Backend\n    *   `/mcp` -\u003e MCP Server\n\n\n### Environment Overlays (Staging/Production)\n\nYou can manage multiple environments (e.g., Staging, Production) using Kustomize overlays located in `k8s/overlays/`.\n\n1.  **Create an Overlay:**\n    Copy an existing overlay (e.g., `k8s/overlays/dev`) to `k8s/overlays/staging` or `k8s/overlays/prod`.\n\n2.  **Customize `kustomization.yaml`:**\n    *   Set the `namespace` for the environment.\n    *   Reference the base resources.\n    *   Add patches for `Ingress` (to set the correct host) and `ConfigMap` (see below).\n\n3.  **Patch `ConfigMap`:**\n    Create a `configmap-patch.yaml` in your overlay directory to override environment-specific values like module URLs.\n    \n    ```yaml\n    apiVersion: v1\n    kind: ConfigMap\n    metadata:\n      name: appointme\n    data:\n      API_URL: \"https://staging.example.com/api/v1\"\n      BASE_URL: \"https://staging.example.com\"\n      REACT_APP_API_URL: \"https://staging.example.com/api/v1\"\n      REACT_APP_URL: \"https://staging.example.com\"\n    ```\n\n4.  **Deploy:**\n    ```bash\n    kubectl apply -k k8s/overlays/staging\n    ```\n\n\n### Deployment with ArgoCD\n\nTo deploy with ArgoCD, you can use the Application manifests provided in `k8s/argocd/`.\n\nExample `k8s/argocd/prod.yaml`:\n\n```yaml\napiVersion: argoproj.io/v1alpha1\nkind: Application\nmetadata:\n  name: appointme-prod\n  namespace: argocd\nspec:\n  project: default\n  source:\n    repoURL: 'https://github.com/fhswf/appointme'\n    targetRevision: HEAD\n    path: k8s/overlays/prod\n  destination:\n    server: https://kubernetes.default.svc\n    namespace: appointme-prod\n  syncPolicy:\n    automated: {}\n  orphanedResources:\n    warn: true # Warn about other unknown resources\n    ignore:\n      - kind: Secret\n        name: \"argocd-secret\"\n```\n\n### Configuration\n\n\n- provide details in `docker.env` and `.env`\n\n#### Configuration Values\n\n| Variable | Description | Required | Default | Source |\n|----------|-------------|----------|---------|--------|\n| `MONGO_URI` | Connection string for MongoDB | Yes | | ConfigMap: `appointme` |\n| `BASE_URL` | URL of the frontend application (e.g., `https://example.com`) | Yes | | ConfigMap: `appointme` |\n| `API_URL` | URL of the backend API (e.g., `https://api.example.com/api/v1`) | Yes | | ConfigMap: `appointme` |\n| `BASE_PATH` | Base path of the application | No | `/` | ConfigMap: `appointme` |\n| `DOMAIN` | Domain for cookie scoping (e.g. `example.com`) | No | | ConfigMap: `appointme` (implied) |\n| `JWT_SECRET` | Secret key for signing JWTs | Yes | | Secret: `appointme-secret` |\n| `CSRF_SECRET` | Secret key for CSRF protection | Yes | | Secret: `appointme-secret` |\n| `ADMIN_API_KEY` | API Key for admin/cron operations | Yes | | Secret: `appointme-secret` |\n| `SENTRY_DSN` | Sentry DSN for error tracking | No | | Secret: `appointme-secret` |\n| `CLIENT_ID` | Google OAuth2 Client ID | No (if Google Login disabled) | | Secret: `appointme-secret` |\n| `CLIENT_SECRET` | Google OAuth2 Client Secret | No (if Google Login disabled) | | Secret: `appointme-secret` |\n| `DISABLE_GOOGLE_LOGIN`| Set to `true` to disable Google Login | No | `false` | ConfigMap: `appointme` |\n| `OIDC_ISSUER` | OIDC Provider URL (e.g., Keycloak Realm URL) | No (if OIDC disabled) | | ConfigMap: `appointme` |\n| `OIDC_CLIENT_ID` | OIDC Client ID | No (if OIDC disabled) | | ConfigMap: `appointme` |\n| `OIDC_CLIENT_SECRET` | OIDC Client Secret (for Confidential clients) | No | | Secret: `appointme-secret` |\n| `EMAIL_FROM` | Email address for sending notifications | Yes | | Secret: `appointme-secret` |\n| `EMAIL_PASSWORD` | Password for the email account | Yes | | Secret: `appointme-secret` |\n| `ENCRYPTION_KEY` | 32-byte hex key for encrypting CalDAV passwords | Yes | | Secret: `appointme-secret` |\n| `CONTACT_INFO` | Contact information (Markdown supported) | No | | ConfigMap: `appointme` |\n| `REACT_APP_API_URL` | Public API URL for the React Client | Yes | | ConfigMap: `appointme` |\n| `REACT_APP_URL` | Public URL of the React Client | Yes | | ConfigMap: `appointme` |\n\n## LTI Integration\n\nAPPointment supports **LTI 1.3** (Learning Tools Interoperability) integration through **OpenID Connect (OIDC)**. This allows the application to be integrated into Learning Management Systems (LMS) like Moodle, Canvas, or other LTI-compliant platforms.\n\n### How It Works\n\nThe LTI integration is implemented using the standard OIDC authentication flow:\n\n1. **Authentication Flow**:\n   - User clicks \"Login with OIDC\" in the application\n   - Client fetches authorization URL from `/api/v1/oidc/url`\n   - User is redirected to the OIDC provider (e.g., Keycloak, LMS LTI endpoint)\n   - After successful authentication, user is redirected back to `/oidc-callback` with authorization code\n   - Client sends code to `/api/v1/oidc/login` endpoint\n   - Backend exchanges code for tokens and retrieves user claims\n   - User is created or updated in the database based on email\n   - JWT token is issued and set as HTTP-only cookie\n\n2. **LTI Role Mapping**:\n   The application automatically maps LTI roles from the OIDC claims to internal application roles:\n   - LTI roles are extracted from `https://purl.imsglobal.org/spec/lti/claim/roles` claim\n   - Users with roles containing \"student\" or \"learner\" (case-insensitive) are assigned the `student` role\n   - Additional role mappings can be extended in [`oidc_controller.ts`](backend/src/controller/oidc_controller.ts)\n\n3. **User Creation**:\n   - Users are identified by their `sub` (subject) claim from the OIDC token\n   - User profile is created/updated with:\n     - Email (required)\n     - Name (from `name` claim or derived from email)\n     - Profile picture (from `picture` claim, if provided)\n     - Roles (mapped from LTI roles)\n   - User URL collisions are automatically handled with random suffixes\n\n### Configuration\n\nTo enable LTI/OIDC authentication, configure the following environment variables:\n\n| Variable | Description | Example | Source |\n|----------|-------------|---------|--------|\n| `OIDC_ISSUER` | OIDC Provider/LTI Platform URL | `https://keycloak.example.com/realms/myrealm` | ConfigMap: `appointme` |\n| `OIDC_CLIENT_ID` | OIDC Client ID registered with the provider | `appointme` | ConfigMap: `appointme` |\n| `OIDC_CLIENT_SECRET` | Client Secret (for confidential clients) | `your-secret-here` | Secret: `appointme-secret` |\n| `OIDC_NAME` | Display name for the login button (optional) | `Campus-ID` | ConfigMap: `appointme` |\n| `OIDC_ICON` | Icon path for the login button (optional) | `/fh-swf.svg` | ConfigMap: `appointme` |\n| `LTI_ISSUER` | LTI Issuer URL (Overrides OIDC_ISSUER for LTI) | `https://moodle.example.com` | ConfigMap: `appointme` |\n| `LTI_CLIENT_ID` | LTI Client ID (Overrides OIDC_CLIENT_ID for LTI) | `client-123` | ConfigMap: `appointme` |\n| `LTI_CLIENT_SECRET` | LTI Client Secret (Overrides OIDC_CLIENT_SECRET) | `secret-456` | Secret (Implicit?) |\n| `LTI_AUTH_ENDPOINT` | LTI Authorization Endpoint | `https://moodle.example.com/mod/lti/auth.php` | ConfigMap: `appointme` |\n| `LTI_TOKEN_ENDPOINT` | LTI Token Endpoint | `https://moodle.example.com/mod/lti/token.php` | ConfigMap: `appointme` |\n| `LTI_JWKS_URI` | LTI JWKS URI | `https://moodle.example.com/mod/lti/certs.php` | ConfigMap: `appointme` |\n\n### Setting Up with Keycloak\n\n1. Create a new client in Keycloak:\n   - Client ID: Your desired client ID (e.g., `appointme`)\n   - Client Protocol: `openid-connect`\n   - Access Type: `confidential` (if using client secret) or `public`\n   - Valid Redirect URIs: `https://your-domain.com/oidc-callback`\n\n2. Configure LTI Claims (if using LTI):\n   - Ensure the client mapper includes `https://purl.imsglobal.org/spec/lti/claim/roles` in the ID token\n   - Map user roles appropriately (e.g., Student, Instructor)\n\n3. Set environment variables in your deployment with the Keycloak realm URL and client credentials\n\n### Setting Up with LMS (Moodle/Canvas)\n \n Most modern LMS platforms support LTI 1.3 with OIDC. When configuring AppointMe as an External Tool (LTI 1.3), use the following settings:\n \n **Moodle Tool Configuration:**\n \n *   **Tool URL**: `[BASE_URL]` (e.g., `https://appointme.example.com`)\n *   **LTI version**: LTI 1.3\n *   **Public key type**: RSA Key\n    *   *AppointMe uses `client_secret_basic` authentication (Client ID + Client Secret) to communicate with the LMS. It DOES NOT sign requests with a private key (which is what `private_key_jwt` uses).*\n    *   *However, some LMS versions (like Moodle) may structurally require a Public Key to be present in the configuration form.*\n    *   *If required, you can generate a \"dummy\" key pair to satisfy the form:*\n        ```bash\n        openssl genrsa -out private.key 2048\n        openssl rsa -in private.key -pubout -out public.key\n        ```\n    *   *Paste `public.key` into Moodle. AppointMe does NOT need the private key and will NOT use this key pair. It validates the JWTs sent BY Moodle using Moodle's own public keys (fetched automatically via the Issuer URL).*\n *   **Initiate login URL**: `[API_URL]/api/v1/oidc/init` (e.g., `https://api.appointme.example.com/api/v1/oidc/init`)\n *   **Redirection URI(s)**: `[BASE_URL]/oidc-callback` (e.g., `https://appointme.example.com/oidc-callback`)\n *   **Custom parameters**: No custom parameters are required.\n     *   *Roles are automatically mapped from `https://purl.imsglobal.org/spec/lti/claim/roles`.*\n \n **Environment Configuration (in AppointMe):**\n \n 1.  Set `OIDC_ISSUER` to the Platform ID / Issuer URL provided by Moodle (e.g., `https://moodle.example.com`).\n 2.  Set `OIDC_CLIENT_ID` to the Client ID generated by Moodle.\n 3.  Set `OIDC_CLIENT_SECRET` to the Client Secret provided by Moodle (or generate one if using specific plugins).\n\n\n### Security Features\n\n- **Rate Limiting**: \n  - Authorization URL endpoint: 100 requests per 15 minutes per IP\n  - Login endpoint: 5 attempts per minute per IP\n- **HTTP-only Cookies**: JWT tokens are stored in secure, HTTP-only cookies\n- **Token Validation**: All tokens are validated using the OIDC provider's public keys (JWKS)\n- **CORS Protection**: Configurable allowed origins via `CORS_ALLOWED_ORIGINS`\n\n### API Endpoints\n\n- `GET /api/v1/oidc/config` - Check if OIDC is enabled\n- `GET /api/v1/oidc/url` - Get authorization URL for authentication\n- `POST /api/v1/oidc/login` - Complete authentication with authorization code\n\n### Technical Implementation\n\nThe LTI/OIDC integration is implemented in:\n- Backend controller: [`backend/src/controller/oidc_controller.ts`](backend/src/controller/oidc_controller.ts)\n- Backend routes: [`backend/src/routes/oidc_routes.ts`](backend/src/routes/oidc_routes.ts)\n- Client callback handler: [`client/src/pages/OidcCallback.tsx`](client/src/pages/OidcCallback.tsx)\n\nFor detailed implementation, see the source code in the repository.\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffhswf%2Fappointme","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffhswf%2Fappointme","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffhswf%2Fappointme/lists"}