├── .github └── workflows │ ├── build-images.yml │ └── ci.yml ├── .gitignore ├── .rustfmt.toml ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── OWNERS ├── README.md ├── charts └── gitjobs │ ├── .helmignore │ ├── Chart.yaml │ ├── LICENSE │ ├── charts │ └── postgresql-16.3.2.tgz │ ├── templates │ ├── _helpers.tpl │ ├── db_migrator_job.yaml │ ├── db_migrator_secret.yaml │ ├── server_deployment.yaml │ ├── server_ingress.yaml │ ├── server_secret.yaml │ ├── server_service.yaml │ ├── server_serviceaccount.yaml │ ├── syncer_cronjob.yaml │ └── syncer_secret.yaml │ └── values.yaml ├── database └── migrations │ ├── Dockerfile │ ├── functions │ ├── 001_load_functions.sql │ ├── auth │ │ └── user_has_image_access.sql │ ├── dashboard │ │ └── search_applications.sql │ ├── img │ │ └── get_image_version.sql │ ├── jobboard │ │ ├── get_stats.sql │ │ ├── search_jobs.sql │ │ └── update_jobs_views.sql │ └── misc │ │ └── search_locations.sql │ ├── migrate.sh │ └── schema │ ├── 0001_initial.sql │ ├── 0002_unique_application.sql │ ├── 0003_job_tz_interval.sql │ ├── 0004_salary_usd_year.sql │ ├── 0005_moderation.sql │ ├── 0006_profile_bluesky_url.sql │ ├── 0007_foundation_landscape_url.sql │ ├── 0008_team_member_management.sql │ ├── 0009_job_views.sql │ ├── 0010_first_published_at.sql │ └── 0011_soft_delete_job.sql ├── docs ├── about.md └── screenshots │ ├── embed_job.svg │ ├── gitjobs1.png │ ├── gitjobs2.png │ ├── gitjobs3.png │ └── gitjobs4.png ├── gitjobs-server ├── Cargo.toml ├── Dockerfile ├── askama.toml ├── build.rs ├── src │ ├── auth.rs │ ├── config.rs │ ├── db │ │ ├── auth.rs │ │ ├── dashboard │ │ │ ├── employer.rs │ │ │ ├── job_seeker.rs │ │ │ ├── mod.rs │ │ │ └── moderator.rs │ │ ├── img.rs │ │ ├── jobboard.rs │ │ ├── misc.rs │ │ ├── mod.rs │ │ ├── notifications.rs │ │ ├── views.rs │ │ └── workers.rs │ ├── handlers │ │ ├── auth.rs │ │ ├── dashboard │ │ │ ├── employer │ │ │ │ ├── applications.rs │ │ │ │ ├── employers.rs │ │ │ │ ├── home.rs │ │ │ │ ├── jobs.rs │ │ │ │ ├── mod.rs │ │ │ │ └── team.rs │ │ │ ├── job_seeker │ │ │ │ ├── applications.rs │ │ │ │ ├── home.rs │ │ │ │ ├── mod.rs │ │ │ │ └── profile.rs │ │ │ ├── mod.rs │ │ │ └── moderator │ │ │ │ ├── home.rs │ │ │ │ ├── jobs.rs │ │ │ │ └── mod.rs │ │ ├── error.rs │ │ ├── extractors.rs │ │ ├── img.rs │ │ ├── jobboard │ │ │ ├── about.rs │ │ │ ├── embed.rs │ │ │ ├── jobs.rs │ │ │ ├── mod.rs │ │ │ └── stats.rs │ │ ├── misc.rs │ │ └── mod.rs │ ├── img │ │ ├── db.rs │ │ └── mod.rs │ ├── main.rs │ ├── notifications.rs │ ├── router.rs │ ├── templates │ │ ├── auth.rs │ │ ├── dashboard │ │ │ ├── employer │ │ │ │ ├── applications.rs │ │ │ │ ├── employers.rs │ │ │ │ ├── home.rs │ │ │ │ ├── jobs.rs │ │ │ │ ├── mod.rs │ │ │ │ └── team.rs │ │ │ ├── job_seeker │ │ │ │ ├── applications.rs │ │ │ │ ├── home.rs │ │ │ │ ├── mod.rs │ │ │ │ └── profile.rs │ │ │ ├── mod.rs │ │ │ └── moderator │ │ │ │ ├── home.rs │ │ │ │ ├── jobs.rs │ │ │ │ └── mod.rs │ │ ├── filters.rs │ │ ├── helpers.rs │ │ ├── jobboard │ │ │ ├── about.rs │ │ │ ├── embed.rs │ │ │ ├── jobs.rs │ │ │ ├── mod.rs │ │ │ └── stats.rs │ │ ├── misc.rs │ │ ├── mod.rs │ │ ├── notifications.rs │ │ ├── pagination.rs │ │ └── testdata │ │ │ ├── job_contractor.golden │ │ │ ├── job_full_details.golden │ │ │ ├── job_internship.golden │ │ │ ├── job_part_time.golden │ │ │ ├── job_remote_no_location.golden │ │ │ ├── job_with_fixed_salary.golden │ │ │ ├── job_with_location_hybrid.golden │ │ │ ├── job_with_location_onsite.golden │ │ │ ├── job_with_location_remote.golden │ │ │ ├── job_with_open_source.golden │ │ │ ├── job_with_salary_min_only.golden │ │ │ ├── job_with_salary_range.golden │ │ │ ├── job_with_seniority.golden │ │ │ ├── job_with_skills.golden │ │ │ ├── job_with_upstream_commitment.golden │ │ │ └── minimal_job.golden │ ├── views.rs │ └── workers.rs ├── static │ ├── css │ │ └── styles.src.css │ ├── images │ │ ├── CNCF_logo_white.svg │ │ ├── background.jpg │ │ ├── badge_member.png │ │ ├── gitjobs.png │ │ ├── icons │ │ │ ├── applications.svg │ │ │ ├── archive.svg │ │ │ ├── arrow_left.svg │ │ │ ├── arrow_left_double.svg │ │ │ ├── arrow_right.svg │ │ │ ├── arrow_right_double.svg │ │ │ ├── bluesky.svg │ │ │ ├── briefcase.svg │ │ │ ├── buildings.svg │ │ │ ├── calendar.svg │ │ │ ├── cancel.svg │ │ │ ├── caret_down.svg │ │ │ ├── caret_up.svg │ │ │ ├── check.svg │ │ │ ├── clipboard.svg │ │ │ ├── close.svg │ │ │ ├── company.svg │ │ │ ├── copy.svg │ │ │ ├── draft.svg │ │ │ ├── email.svg │ │ │ ├── eraser.svg │ │ │ ├── external_link.svg │ │ │ ├── eye.svg │ │ │ ├── facebook.svg │ │ │ ├── file_badge.svg │ │ │ ├── filter.svg │ │ │ ├── gear.svg │ │ │ ├── github.svg │ │ │ ├── graduation_cap.svg │ │ │ ├── hour_glass.svg │ │ │ ├── image.svg │ │ │ ├── info.svg │ │ │ ├── lfx.svg │ │ │ ├── link.svg │ │ │ ├── linkedin.svg │ │ │ ├── list.svg │ │ │ ├── live.svg │ │ │ ├── location.svg │ │ │ ├── login.svg │ │ │ ├── logout.svg │ │ │ ├── medal.svg │ │ │ ├── menu.svg │ │ │ ├── microphone.svg │ │ │ ├── money.svg │ │ │ ├── office.svg │ │ │ ├── office_chair.svg │ │ │ ├── organigram.svg │ │ │ ├── outline_clipboard.svg │ │ │ ├── pencil.svg │ │ │ ├── pending_invitation.svg │ │ │ ├── phone.svg │ │ │ ├── plus.svg │ │ │ ├── plus_bottom.svg │ │ │ ├── plus_top.svg │ │ │ ├── project.svg │ │ │ ├── remote.svg │ │ │ ├── save.svg │ │ │ ├── search.svg │ │ │ ├── send.svg │ │ │ ├── signature.svg │ │ │ ├── stats.svg │ │ │ ├── tasks.svg │ │ │ ├── team.svg │ │ │ ├── trash.svg │ │ │ ├── twitter.svg │ │ │ ├── user.svg │ │ │ ├── user_plus.svg │ │ │ ├── vertical_dots.svg │ │ │ └── warning.svg │ │ ├── index │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon.ico │ │ │ ├── favicon.svg │ │ │ └── gitjobs.png │ │ └── spinner │ │ │ ├── spinner_1.svg │ │ │ ├── spinner_2.svg │ │ │ ├── spinner_3.svg │ │ │ ├── spinner_4.svg │ │ │ └── spinner_5.svg │ ├── js │ │ ├── .prettierrc.yaml │ │ ├── common │ │ │ ├── alerts.js │ │ │ ├── common.js │ │ │ ├── data.js │ │ │ ├── header.js │ │ │ ├── input-range.js │ │ │ ├── lit-wrapper.js │ │ │ ├── markdown-editor.js │ │ │ ├── multiselect.js │ │ │ ├── search-location.js │ │ │ ├── search-projects.js │ │ │ └── searchable-filter.js │ │ ├── dashboard │ │ │ ├── base.js │ │ │ ├── common │ │ │ │ └── dashboard-search.js │ │ │ ├── employer │ │ │ │ └── jobs.js │ │ │ └── jobseeker │ │ │ │ ├── certifications.js │ │ │ │ ├── education.js │ │ │ │ ├── experience.js │ │ │ │ ├── form.js │ │ │ │ └── projects.js │ │ └── jobboard │ │ │ ├── filters.js │ │ │ ├── job_section.js │ │ │ └── stats.js │ └── vendor │ │ ├── css │ │ └── easymde.v2.20.0.min.css │ │ ├── fonts │ │ ├── inter-latin-ext.woff2 │ │ └── inter-latin.woff2 │ │ └── js │ │ ├── easymde.v2.20.0.min.js │ │ ├── echarts.v5.6.0.min.js │ │ ├── htmx.v2.0.4.min.js │ │ ├── lit-all.v3.2.1.min.js │ │ ├── open-iframe-resizer.v1.3.1.min.js │ │ └── sweetalert2.v11.17.2.min.js └── templates │ ├── .djlintrc │ ├── auth │ ├── log_in.html │ ├── sign_up.html │ └── update_user.html │ ├── base.html │ ├── common_base.html │ ├── common_header.html │ ├── dashboard │ ├── dashboard_base.html │ ├── dashboard_base_moderator.html │ ├── dashboard_macros.html │ ├── employer │ │ ├── applications │ │ │ └── list.html │ │ ├── employers │ │ │ ├── add.html │ │ │ ├── initial_setup.html │ │ │ └── update.html │ │ ├── home.html │ │ ├── jobs │ │ │ ├── add.html │ │ │ ├── list.html │ │ │ ├── preview.html │ │ │ └── update.html │ │ └── teams │ │ │ ├── invitations_list.html │ │ │ └── members_list.html │ ├── job_seeker │ │ ├── applications │ │ │ └── list.html │ │ ├── home.html │ │ └── profile │ │ │ ├── preview.html │ │ │ └── update.html │ └── moderator │ │ ├── home.html │ │ ├── live_jobs.html │ │ ├── moderator_macros.html │ │ └── pending_jobs.html │ ├── footer.html │ ├── header.html │ ├── jobboard │ ├── about │ │ └── page.html │ ├── embed │ │ ├── job_card.svg │ │ └── jobs_page.html │ ├── jobs │ │ ├── explore_section.html │ │ ├── job_section.html │ │ ├── jobs.html │ │ ├── jobs_macros.html │ │ └── results_section.html │ └── stats │ │ └── page.html │ ├── macros.html │ ├── misc │ ├── job_preview.html │ ├── not_found.html │ ├── preview_modal.html │ └── user_menu_section.html │ ├── navigation_links.html │ └── notifications │ ├── base.html │ ├── email_macros.html │ ├── email_verification.html │ ├── slack_job_published.md │ └── team_invitation.html └── gitjobs-syncer ├── Cargo.toml ├── Dockerfile └── src ├── config.rs ├── db.rs ├── main.rs └── syncer.rs /.github/workflows/build-images.yml: -------------------------------------------------------------------------------- 1 | name: Build images 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-dbmigrator-image: 10 | if: github.ref == 'refs/heads/main' 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | - name: Configure AWS credentials 16 | uses: aws-actions/configure-aws-credentials@v4 17 | with: 18 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 19 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 20 | aws-region: us-east-2 21 | - name: Login to AWS ECR 22 | id: login-ecr 23 | uses: aws-actions/amazon-ecr-login@v2 24 | - name: Build and push gitjobs-dbmigrator image 25 | env: 26 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 27 | run: | 28 | docker build -f database/migrations/Dockerfile -t $ECR_REGISTRY/gitjobs/dbmigrator:$GITHUB_SHA . 29 | docker push $ECR_REGISTRY/gitjobs/dbmigrator:$GITHUB_SHA 30 | 31 | build-server-image: 32 | if: github.ref == 'refs/heads/main' 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout code 36 | uses: actions/checkout@v4 37 | - name: Configure AWS credentials 38 | uses: aws-actions/configure-aws-credentials@v4 39 | with: 40 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 41 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 42 | aws-region: us-east-2 43 | - name: Login to AWS ECR 44 | id: login-ecr 45 | uses: aws-actions/amazon-ecr-login@v2 46 | - name: Build and push gitjobs-server image 47 | env: 48 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 49 | run: | 50 | docker build -f gitjobs-server/Dockerfile -t $ECR_REGISTRY/gitjobs/server:$GITHUB_SHA . 51 | docker push $ECR_REGISTRY/gitjobs/server:$GITHUB_SHA 52 | 53 | build-syncer-image: 54 | if: github.ref == 'refs/heads/main' 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: Checkout code 58 | uses: actions/checkout@v4 59 | - name: Configure AWS credentials 60 | uses: aws-actions/configure-aws-credentials@v4 61 | with: 62 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 63 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 64 | aws-region: us-east-2 65 | - name: Login to AWS ECR 66 | id: login-ecr 67 | uses: aws-actions/amazon-ecr-login@v2 68 | - name: Build and push gitjobs-syncer image 69 | env: 70 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 71 | run: | 72 | docker build -f gitjobs-syncer/Dockerfile -t $ECR_REGISTRY/gitjobs/syncer:$GITHUB_SHA . 73 | docker push $ECR_REGISTRY/gitjobs/syncer:$GITHUB_SHA 74 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | lint-and-test-server: 10 | runs-on: 11 | labels: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | - name: Setup Rust 16 | uses: dtolnay/rust-toolchain@master 17 | with: 18 | toolchain: 1.86.0 19 | components: clippy, rustfmt 20 | - name: Install Tailwind CSS 21 | run: | 22 | wget -O /usr/local/bin/tailwindcss https://github.com/tailwindlabs/tailwindcss/releases/download/v4.0.17/tailwindcss-linux-x64 23 | chmod +x /usr/local/bin/tailwindcss 24 | - name: Run clippy 25 | run: cargo clippy --all-targets --all-features -- --deny warnings 26 | - name: Run rustfmt 27 | run: cargo fmt --all -- --check 28 | - name: Run tests 29 | run: cargo test 30 | 31 | lint-templates: 32 | runs-on: 33 | labels: ubuntu-latest 34 | steps: 35 | - name: Checkout code 36 | uses: actions/checkout@v4 37 | - name: Install djlint 38 | run: pip install djlint==1.36.4 39 | - name: Run djlint 40 | run: | 41 | djlint \ 42 | --reformat \ 43 | --configuration gitjobs-server/templates/.djlintrc \ 44 | gitjobs-server/templates 45 | 46 | check-js-files-format: 47 | runs-on: 48 | labels: ubuntu-latest 49 | steps: 50 | - name: Checkout code 51 | uses: actions/checkout@v4 52 | - name: Run prettier 53 | uses: creyD/prettier_action@v4.3 54 | with: 55 | dry: true 56 | prettier_options: --check gitjobs-server/static/js/**/*.js --config gitjobs-server/static/js/.prettierrc.yaml 57 | 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | Chart.lock 4 | chart/charts 5 | gitjobs-server/static/css/styles.css 6 | /target 7 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 110 2 | chain_width = 70 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "gitjobs-server", 5 | "gitjobs-syncer" 6 | ] 7 | 8 | [workspace.package] 9 | version = "0.0.1" 10 | license = "Apache-2.0" 11 | edition = "2024" 12 | rust-version = "1.86" 13 | 14 | [workspace.dependencies] 15 | anyhow = "1.0.98" 16 | askama = { version = "0.14.0", features = ["serde_json"] } 17 | async-trait = "0.1.88" 18 | axum = { version = "0.8.4", features = ["macros", "multipart"] } 19 | axum-login = "0.17.0" 20 | axum-extra = { version = "0.10.1", features = ["form"] } 21 | axum-messages = "0.8.0" 22 | cached = { version = "0.55.1", features = ["async"] } 23 | clap = { version = "4.5.37", features = ["derive"] } 24 | chrono = { version = "0.4.41", features = ["serde"] } 25 | chrono-tz = { version = "0.10.3", features = ["serde"] } 26 | deadpool-postgres = { version = "0.14.1", features = ["serde"] } 27 | emojis = "0.6.4" 28 | figment = { version = "0.10.19", features = ["yaml", "env"] } 29 | futures = "0.3.31" 30 | human_format = "1.1.0" 31 | image = "0.25.6" 32 | lettre = { version = "0.11.15", default-features = false, features = ["builder", "hostname", "pool", "smtp-transport", "tokio1-rustls-tls"] } 33 | markdown = "1.0.0-alpha.24" 34 | mime_guess = "2.0.5" 35 | minify-html = "0.16.4" 36 | mockall = "0.13.1" 37 | num-format = "0.4.4" 38 | oauth2 = "5.0.0" 39 | openidconnect = { version = "4.0.0", features = ["accept-rfc3339-timestamps"] } 40 | openssl = { version = "0.10.72", features = ["vendored"] } 41 | palette = "0.7.6" 42 | password-auth = "1.0.0" 43 | postgres-openssl = "0.5.1" 44 | rand = "0.9.1" 45 | regex = "1.11.1" 46 | reqwest = { version = "0.12.15", features = ["json"] } 47 | rust-embed = "8.7.0" 48 | serde = { version = "1.0.219", features = ["derive"] } 49 | serde_html_form = "0.2.7" 50 | serde_json = "1.0.140" 51 | serde_qs = { version = "0.15.0", features = ["axum"] } 52 | serde_with = "3.12.0" 53 | strum = { version = "0.27", features = ["derive"] } 54 | thiserror = "2.0.12" 55 | time = "0.3.41" 56 | tokio = { version = "1.44.2", features = [ 57 | "macros", 58 | "process", 59 | "rt-multi-thread", 60 | "signal", 61 | "sync", 62 | "time", 63 | ] } 64 | tokio-postgres = { version = "0.7.13", features = [ 65 | "with-chrono-0_4", 66 | "with-serde_json-1", 67 | "with-time-0_3", 68 | "with-uuid-1", 69 | ] } 70 | tokio-util = { version = "0.7.15", features = ["full"] } 71 | tower = "0.5.2" 72 | tower-http = { version = "0.6.2", features = ["auth", "fs", "set-header", "trace"] } 73 | tower-sessions = { version = "0.14.0", features = ["signed"] } 74 | tracing = "0.1.41" 75 | tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] } 76 | unicode-segmentation = "1.12.0" 77 | uuid = { version = "1.16.0", features = ["serde", "v4"] } 78 | which = "7.0.3" 79 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | maintainers: 2 | - tegioz 3 | - cynthia-sg 4 | - caniszczyk 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitJobs 2 | 3 | **GitJobs** is a simple open source, developer first job board focused on: 4 | 5 | * open source job opportunities filterable by language, technology and foundation 6 | * highlighting the opportunities that contribute back to open source projects 7 | * encouraging sustainability by contributing back to upstream projects 8 | 9 | > [!WARNING] 10 | > This project is currently in the early stages of development and is in beta. 11 | 12 | | ![Screenshot 1](docs/screenshots/gitjobs1.png?raw=true) | ![Screenshot 2](docs/screenshots/gitjobs2.png?raw=true) | 13 | | ------------------------------------------------------- | ------------------------------------------------------- | 14 | | ![Screenshot 3](docs/screenshots/gitjobs3.png?raw=true) | ![Screenshot 4](docs/screenshots/gitjobs4.png?raw=true) | 15 | 16 | ## Embed 17 | 18 | **GitJobs** allows other websites to embed a view to display the jobs that match certain criteria. You can, for example, list jobs that offer time to work on a specific project, or jobs related to any of the projects in a given foundation. Any criteria supported by the filters, including the text search input, can be used to create an embed view. The embed code is a simple iframe that can be added to any website. 19 | 20 | To setup yours, the first step is to adjust the filters as if you were searching for jobs. Once you have the desired filters, you can click on the `Get embed code` button, at the bottom of the filters. This will open a modal with the ready to use embed code. 21 | 22 | You can see it in action in this [example](https://codepen.io/cynthiasg/pen/gbOJLOb). 23 | 24 | ### Embedding a single job 25 | 26 | It is also possible to **embed a single job**. To do this, you can click on the `Get embed code` button in the job details page (in the *share* section). The embed code is available in *Markdown*, *AsciiDoc* and *HTML* formats. The job card generated is an SVG image, and the code will include a link to the job at . 27 | 28 | ![Single job embed](docs/screenshots/embed_job.svg) 29 | 30 | ## Contributing 31 | 32 | Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for more details. 33 | 34 | ## Code of Conduct 35 | 36 | This project follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 37 | 38 | ## License 39 | 40 | **GitJobs** is an open source project licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). 41 | -------------------------------------------------------------------------------- /charts/gitjobs/.helmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .git/ 3 | .gitignore 4 | *.swp 5 | .vscode/ 6 | -------------------------------------------------------------------------------- /charts/gitjobs/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: gitjobs 3 | description: GitJobs is an open source job board platform 4 | type: application 5 | version: 0.1.1-0 6 | appVersion: 0.1.0 7 | kubeVersion: ">= 1.19.0-0" 8 | keywords: 9 | - cncf 10 | - job 11 | - board 12 | maintainers: 13 | - name: Sergio 14 | email: tegioz@icloud.com 15 | - name: Cintia 16 | email: cynthiasg@icloud.com 17 | dependencies: 18 | - name: postgresql 19 | version: 16.3.2 20 | repository: https://charts.bitnami.com/bitnami 21 | condition: postgresql.enabled 22 | -------------------------------------------------------------------------------- /charts/gitjobs/charts/postgresql-16.3.2.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cncf/gitjobs/7835dc18e436d262635d1b11f3caebcbc9685657/charts/gitjobs/charts/postgresql-16.3.2.tgz -------------------------------------------------------------------------------- /charts/gitjobs/templates/db_migrator_job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | {{- if .Release.IsInstall }} 5 | name: {{ include "chart.resourceNamePrefix" . }}dbmigrator-install 6 | {{- else }} 7 | name: {{ include "chart.resourceNamePrefix" . }}dbmigrator-upgrade 8 | annotations: 9 | "helm.sh/hook": pre-upgrade 10 | "helm.sh/hook-weight": "0" 11 | "helm.sh/hook-delete-policy": before-hook-creation 12 | {{- end }} 13 | spec: 14 | template: 15 | spec: 16 | {{- with .Values.imagePullSecrets }} 17 | imagePullSecrets: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | restartPolicy: Never 21 | initContainers: 22 | - {{- include "chart.checkDbIsReadyInitContainer" . | nindent 10 }} 23 | containers: 24 | - name: dbmigrator 25 | image: {{ .Values.dbmigrator.job.image.repository }}:{{ .Values.imageTag | default (printf "v%s" .Chart.AppVersion) }} 26 | imagePullPolicy: {{ .Values.pullPolicy }} 27 | env: 28 | - name: TERN_CONF 29 | value: {{ .Values.configDir }}/tern.conf 30 | volumeMounts: 31 | - name: dbmigrator-config 32 | mountPath: {{ .Values.configDir }} 33 | readOnly: true 34 | command: ["./migrate.sh"] 35 | volumes: 36 | - name: dbmigrator-config 37 | secret: 38 | secretName: {{ include "chart.resourceNamePrefix" . }}dbmigrator-config 39 | -------------------------------------------------------------------------------- /charts/gitjobs/templates/db_migrator_secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "chart.resourceNamePrefix" . }}dbmigrator-config 5 | type: Opaque 6 | stringData: 7 | tern.conf: |- 8 | [database] 9 | host = {{ default (printf "%s-postgresql.%s" .Release.Name .Release.Namespace) .Values.db.host }} 10 | port = {{ .Values.db.port }} 11 | database = {{ .Values.db.dbname }} 12 | user = {{ .Values.db.user }} 13 | password = {{ .Values.db.password }} 14 | sslmode = prefer 15 | -------------------------------------------------------------------------------- /charts/gitjobs/templates/server_deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "chart.resourceNamePrefix" . }}server 5 | labels: 6 | app.kubernetes.io/component: server 7 | {{- include "chart.labels" . | nindent 4 }} 8 | spec: 9 | replicas: {{ .Values.server.deploy.replicaCount }} 10 | selector: 11 | matchLabels: 12 | app.kubernetes.io/component: server 13 | {{- include "chart.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | labels: 17 | app.kubernetes.io/component: server 18 | {{- include "chart.selectorLabels" . | nindent 8 }} 19 | spec: 20 | {{- with .Values.imagePullSecrets }} 21 | imagePullSecrets: 22 | {{- toYaml . | nindent 8 }} 23 | {{- end }} 24 | {{- if .Release.IsInstall }} 25 | serviceAccountName: {{ include "chart.resourceNamePrefix" . }}server 26 | {{- end }} 27 | initContainers: 28 | - {{- include "chart.checkDbIsReadyInitContainer" . | nindent 10 }} 29 | {{- if .Release.IsInstall }} 30 | - name: check-dbmigrator-run 31 | image: "bitnami/kubectl:{{ template "chart.KubernetesVersion" . }}" 32 | imagePullPolicy: IfNotPresent 33 | command: ['kubectl', 'wait', '--namespace={{ .Release.Namespace }}', '--for=condition=complete', 'job/{{ include "chart.resourceNamePrefix" . }}dbmigrator-install', '--timeout=60s'] 34 | {{- end }} 35 | containers: 36 | - name: server 37 | image: {{ .Values.server.deploy.image.repository }}:{{ .Values.imageTag | default (printf "v%s" .Chart.AppVersion) }} 38 | imagePullPolicy: {{ .Values.pullPolicy }} 39 | volumeMounts: 40 | - name: server-config 41 | mountPath: {{ .Values.configDir | quote }} 42 | readOnly: true 43 | ports: 44 | - name: http 45 | containerPort: 9000 46 | protocol: TCP 47 | resources: 48 | {{- toYaml .Values.server.deploy.resources | nindent 12 }} 49 | command: ['gitjobs-server', '-c', '{{ .Values.configDir }}/server.yml'] 50 | volumes: 51 | - name: server-config 52 | secret: 53 | secretName: {{ include "chart.resourceNamePrefix" . }}server-config 54 | -------------------------------------------------------------------------------- /charts/gitjobs/templates/server_ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.server.ingress.enabled -}} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: {{ include "chart.resourceNamePrefix" . }}server 6 | labels: 7 | app.kubernetes.io/component: server 8 | {{- include "chart.labels" . | nindent 4 }} 9 | {{- with .Values.server.ingress.annotations }} 10 | annotations: 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | spec: 14 | defaultBackend: 15 | service: 16 | name: {{ include "chart.resourceNamePrefix" . }}server 17 | port: 18 | number: {{ .Values.server.service.port }} 19 | {{- with .Values.server.ingress.rules }} 20 | rules: 21 | {{- toYaml . | nindent 4 }} 22 | {{- end }} 23 | {{- with .Values.server.ingress.tls }} 24 | tls: 25 | {{- toYaml . | nindent 4 }} 26 | {{- end }} 27 | {{- end }} 28 | -------------------------------------------------------------------------------- /charts/gitjobs/templates/server_secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "chart.resourceNamePrefix" . }}server-config 5 | type: Opaque 6 | stringData: 7 | server.yml: |- 8 | db: 9 | host: {{ default (printf "%s-postgresql.%s" .Release.Name .Release.Namespace) .Values.db.host }} 10 | port: {{ .Values.db.port }} 11 | dbname: {{ .Values.db.dbname }} 12 | user: {{ .Values.db.user }} 13 | password: {{ .Values.db.password }} 14 | email: 15 | from_address: {{ .Values.email.fromAddress }} 16 | from_name: {{ .Values.email.fromName }} 17 | smtp: 18 | host: {{ .Values.email.smtp.host }} 19 | port: {{ .Values.email.smtp.port }} 20 | username: {{ .Values.email.smtp.username }} 21 | password: {{ .Values.email.smtp.password }} 22 | log: 23 | format: {{ .Values.log.format }} 24 | server: 25 | addr: {{ .Values.server.addr }} 26 | analytics: 27 | google_tag_id: {{ .Values.server.analytics.googleTagId }} 28 | osano_script_url: {{ .Values.server.analytics.osanoScriptUrl }} 29 | base_url: {{ .Values.server.baseUrl }} 30 | basic_auth: 31 | enabled: {{ .Values.server.basicAuth.enabled }} 32 | username: {{ .Values.server.basicAuth.username }} 33 | password: {{ .Values.server.basicAuth.password }} 34 | cookie: 35 | secure: {{ .Values.server.cookie.secure }} 36 | login: 37 | email: {{ .Values.server.login.email }} 38 | github: {{ .Values.server.login.github }} 39 | linuxfoundation: {{ .Values.server.login.linuxfoundation }} 40 | oauth2: 41 | github: 42 | auth_url: {{ .Values.server.oauth2.github.authUrl }} 43 | client_id: {{ .Values.server.oauth2.github.clientId | quote }} 44 | client_secret: {{ .Values.server.oauth2.github.clientSecret | quote }} 45 | redirect_uri: {{ .Values.server.oauth2.github.redirectUri }} 46 | scopes: {{ .Values.server.oauth2.github.scopes }} 47 | token_url: {{ .Values.server.oauth2.github.tokenUrl }} 48 | oidc: 49 | linuxfoundation: 50 | client_id: {{ .Values.server.oidc.linuxfoundation.clientId | quote }} 51 | client_secret: {{ .Values.server.oidc.linuxfoundation.clientSecret | quote }} 52 | issuer_url: {{ .Values.server.oidc.linuxfoundation.issuerUrl }} 53 | redirect_uri: {{ .Values.server.oidc.linuxfoundation.redirectUri }} 54 | scopes: {{ .Values.server.oidc.linuxfoundation.scopes }} 55 | slack_webhook_url: {{ .Values.server.slackWebhookUrl }} 56 | -------------------------------------------------------------------------------- /charts/gitjobs/templates/server_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "chart.resourceNamePrefix" . }}server 5 | labels: 6 | app.kubernetes.io/component: server 7 | {{- include "chart.labels" . | nindent 4 }} 8 | spec: 9 | type: {{ .Values.server.service.type }} 10 | ports: 11 | - port: {{ .Values.server.service.port }} 12 | targetPort: http 13 | protocol: TCP 14 | name: http 15 | selector: 16 | app.kubernetes.io/component: server 17 | {{- include "chart.selectorLabels" . | nindent 4 }} 18 | -------------------------------------------------------------------------------- /charts/gitjobs/templates/server_serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "chart.resourceNamePrefix" . }}server 5 | --- 6 | apiVersion: rbac.authorization.k8s.io/v1 7 | kind: Role 8 | metadata: 9 | name: {{ include "chart.resourceNamePrefix" . }}job-reader 10 | rules: 11 | - apiGroups: ["batch"] 12 | resources: ["jobs"] 13 | verbs: ["get", "list", "watch"] 14 | --- 15 | apiVersion: rbac.authorization.k8s.io/v1 16 | kind: RoleBinding 17 | metadata: 18 | name: {{ include "chart.resourceNamePrefix" . }}server-job-reader 19 | subjects: 20 | - kind: ServiceAccount 21 | name: {{ include "chart.resourceNamePrefix" . }}server 22 | roleRef: 23 | kind: Role 24 | name: {{ include "chart.resourceNamePrefix" . }}job-reader 25 | apiGroup: rbac.authorization.k8s.io 26 | -------------------------------------------------------------------------------- /charts/gitjobs/templates/syncer_cronjob.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Capabilities.APIVersions.Has "batch/v1/CronJob" }} 2 | apiVersion: batch/v1 3 | {{- else }} 4 | apiVersion: batch/v1beta1 5 | {{- end }} 6 | kind: CronJob 7 | metadata: 8 | name: {{ include "chart.resourceNamePrefix" . }}syncer 9 | spec: 10 | schedule: "0 10 * * *" 11 | successfulJobsHistoryLimit: 1 12 | failedJobsHistoryLimit: 1 13 | concurrencyPolicy: Forbid 14 | jobTemplate: 15 | spec: 16 | template: 17 | spec: 18 | {{- with .Values.imagePullSecrets }} 19 | imagePullSecrets: 20 | {{- toYaml . | nindent 12 }} 21 | {{- end }} 22 | restartPolicy: Never 23 | initContainers: 24 | - {{- include "chart.checkDbIsReadyInitContainer" . | nindent 14 }} 25 | containers: 26 | - name: syncer 27 | image: {{ .Values.syncer.cronjob.image.repository }}:{{ .Values.imageTag | default (printf "v%s" .Chart.AppVersion) }} 28 | imagePullPolicy: {{ .Values.pullPolicy }} 29 | resources: 30 | {{- toYaml .Values.syncer.cronjob.resources | nindent 16 }} 31 | volumeMounts: 32 | - name: syncer-config 33 | mountPath: {{ .Values.configDir | quote }} 34 | readOnly: true 35 | command: ['gitjobs-syncer', '-c', '{{ .Values.configDir }}/syncer.yml'] 36 | volumes: 37 | - name: syncer-config 38 | secret: 39 | secretName: {{ include "chart.resourceNamePrefix" . }}syncer-config 40 | -------------------------------------------------------------------------------- /charts/gitjobs/templates/syncer_secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{ include "chart.resourceNamePrefix" . }}syncer-config 5 | type: Opaque 6 | stringData: 7 | syncer.yml: |- 8 | db: 9 | host: {{ default (printf "%s-postgresql.%s" .Release.Name .Release.Namespace) .Values.db.host }} 10 | port: {{ .Values.db.port }} 11 | dbname: {{ .Values.db.dbname }} 12 | user: {{ .Values.db.user }} 13 | password: {{ .Values.db.password }} 14 | log: 15 | format: {{ .Values.log.format }} 16 | -------------------------------------------------------------------------------- /database/migrations/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build tern 2 | FROM golang:1.24.2-alpine3.21 AS tern 3 | RUN apk --no-cache add git 4 | RUN go install github.com/jackc/tern@latest 5 | 6 | # Build final image 7 | FROM alpine:3.21.3 8 | RUN addgroup -S gitjobs && adduser -S gitjobs -G gitjobs 9 | USER gitjobs 10 | WORKDIR /home/gitjobs 11 | COPY --from=tern /go/bin/tern /usr/local/bin 12 | COPY database/migrations . 13 | -------------------------------------------------------------------------------- /database/migrations/functions/001_load_functions.sql: -------------------------------------------------------------------------------- 1 | {{ template "auth/user_has_image_access.sql" }} 2 | {{ template "dashboard/search_applications.sql" }} 3 | {{ template "img/get_image_version.sql" }} 4 | {{ template "jobboard/get_stats.sql" }} 5 | {{ template "jobboard/search_jobs.sql" }} 6 | {{ template "jobboard/update_jobs_views.sql" }} 7 | {{ template "misc/search_locations.sql" }} 8 | 9 | ---- create above / drop below ---- 10 | 11 | -- Nothing to do 12 | -------------------------------------------------------------------------------- /database/migrations/functions/auth/user_has_image_access.sql: -------------------------------------------------------------------------------- 1 | -- Check if the user has access to the image provided. 2 | create or replace function user_has_image_access(p_user_id uuid, p_image_id uuid) 3 | returns boolean as $$ 4 | begin 5 | -- User is a moderator 6 | perform from "user" 7 | where user_id = p_user_id 8 | and moderator = true; 9 | if found then return true; end if; 10 | 11 | -- Profile photo or employer logo: user created the image 12 | perform from image 13 | where image_id = p_image_id 14 | and created_by = p_user_id; 15 | if found then return true; end if; 16 | 17 | -- Profile photo: applied to a employer's job 18 | perform from job_seeker_profile p 19 | join application a on p.job_seeker_profile_id = a.job_seeker_profile_id 20 | join job j on a.job_id = j.job_id 21 | join employer_team et on j.employer_id = et.employer_id 22 | where p.photo_id = p_image_id 23 | and et.user_id = p_user_id 24 | and et.approved = true; 25 | if found then return true; end if; 26 | 27 | -- Employer logo: user belongs to the employer team 28 | perform from employer e 29 | join employer_team et using (employer_id) 30 | where e.logo_id = p_image_id 31 | and et.user_id = p_user_id 32 | and et.approved = true; 33 | return found; 34 | end 35 | $$ language plpgsql; 36 | -------------------------------------------------------------------------------- /database/migrations/functions/dashboard/search_applications.sql: -------------------------------------------------------------------------------- 1 | -- Returns the applications that match the filters provided. 2 | create or replace function search_applications( 3 | p_employer_id uuid, 4 | p_filters jsonb 5 | ) 6 | returns table(applications json, total bigint) as $$ 7 | declare 8 | v_job_id uuid := (p_filters->>'job_id')::uuid; 9 | v_limit int := coalesce((p_filters->>'limit')::int, 20); 10 | v_offset int := coalesce((p_filters->>'offset')::int, 0); 11 | begin 12 | return query 13 | with filtered_applications as ( 14 | select 15 | a.application_id, 16 | a.created_at as applied_at, 17 | j.job_id, 18 | j.title as job_title, 19 | ( 20 | select nullif(jsonb_strip_nulls(jsonb_build_object( 21 | 'location_id', l.location_id, 22 | 'city', l.city, 23 | 'country', l.country, 24 | 'state', l.state 25 | )), '{}'::jsonb) 26 | ) as job_location, 27 | j.workplace as job_workplace, 28 | p.job_seeker_profile_id, 29 | p.photo_id, 30 | p.name, 31 | ( 32 | select format( 33 | '%s at %s', experience->>'title', experience->>'company' 34 | ) as last_position 35 | from ( 36 | select jsonb_array_elements(p.experience) as experience 37 | ) 38 | order by (experience->>'end_date')::date desc nulls first 39 | limit 1 40 | ) as last_position 41 | from application a 42 | join job j on a.job_id = j.job_id 43 | join job_seeker_profile p on a.job_seeker_profile_id = p.job_seeker_profile_id 44 | left join location l on j.location_id = l.location_id 45 | where j.employer_id = p_employer_id 46 | and j.status <> 'deleted' 47 | and 48 | case when v_job_id is not null then 49 | a.job_id = v_job_id else true end 50 | ) 51 | select 52 | ( 53 | select coalesce(json_agg(json_build_object( 54 | 'application_id', application_id, 55 | 'applied_at', applied_at, 56 | 'job_id', job_id, 57 | 'job_title', job_title, 58 | 'job_location', job_location, 59 | 'job_seeker_profile_id', job_seeker_profile_id, 60 | 'job_workplace', job_workplace, 61 | 'photo_id', photo_id, 62 | 'name', name, 63 | 'last_position', last_position 64 | )), '[]') 65 | from ( 66 | select * 67 | from filtered_applications 68 | order by applied_at desc 69 | limit v_limit 70 | offset v_offset 71 | ) filtered_applications_page 72 | ), 73 | ( 74 | select count(*) from filtered_applications 75 | ); 76 | end 77 | $$ language plpgsql; 78 | -------------------------------------------------------------------------------- /database/migrations/functions/img/get_image_version.sql: -------------------------------------------------------------------------------- 1 | -- Returns an image version. We'll try first to get the version of the size 2 | -- requested. If it doesn't exist, we'll return the svg version (if available). 3 | create or replace function get_image_version(p_image_id uuid, p_version text) 4 | returns table(data bytea, format text) as $$ 5 | begin 6 | -- PNG 7 | return query select iv.data, 'png' as format from image_version iv 8 | where image_id = p_image_id and version = p_version; 9 | if found then return; end if; 10 | 11 | -- SVG 12 | return query select iv.data, 'svg' as format from image_version iv 13 | where image_id = p_image_id and version = 'svg'; 14 | end 15 | $$ language plpgsql; 16 | -------------------------------------------------------------------------------- /database/migrations/functions/jobboard/update_jobs_views.sql: -------------------------------------------------------------------------------- 1 | -- update_jobs_views updates the views of the jobs provided. 2 | create or replace function update_jobs_views(p_lock_key bigint, p_data jsonb) 3 | returns void as $$ 4 | -- Make sure only one batch of updates is processed at a time 5 | select pg_advisory_xact_lock(p_lock_key); 6 | 7 | -- Insert or update the corresponding views counters as needed 8 | insert into job_views (job_id, day, total) 9 | select views_batch.* 10 | from ( 11 | select 12 | (value->>0)::uuid as job_id, 13 | (value->>1)::date as day, 14 | (value->>2)::integer as total 15 | from jsonb_array_elements(p_data) 16 | ) as views_batch 17 | join job on job.job_id = views_batch.job_id 18 | where job.status = 'published' 19 | on conflict (job_id, day) do 20 | update set total = job_views.total + excluded.total; 21 | $$ language sql; 22 | -------------------------------------------------------------------------------- /database/migrations/functions/misc/search_locations.sql: -------------------------------------------------------------------------------- 1 | -- Returns the locations that match the query provided. 2 | create or replace function search_locations(p_ts_query text) 3 | returns table( 4 | location_id uuid, 5 | city text, 6 | country text, 7 | state text 8 | ) as $$ 9 | declare 10 | v_ts_query_with_prefix_matching tsquery; 11 | begin 12 | -- Prepare ts query with prefix matching 13 | select ts_rewrite( 14 | websearch_to_tsquery(p_ts_query), 15 | format( 16 | ' 17 | select 18 | to_tsquery(lexeme), 19 | to_tsquery(lexeme || '':*'') 20 | from unnest(tsvector_to_array(to_tsvector(%L))) as lexeme 21 | ', p_ts_query 22 | ) 23 | ) into v_ts_query_with_prefix_matching; 24 | 25 | return query 26 | select 27 | lws.location_id, 28 | lws.city, 29 | lws.country, 30 | lws.state 31 | from ( 32 | select 33 | l.location_id, 34 | l.city, 35 | l.country, 36 | l.state, 37 | ts_rank(tsdoc, v_ts_query_with_prefix_matching, 1) as score 38 | from location l 39 | where v_ts_query_with_prefix_matching @@ tsdoc 40 | order by score desc 41 | limit 20 42 | ) as lws; 43 | end 44 | $$ language plpgsql; 45 | -------------------------------------------------------------------------------- /database/migrations/migrate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | schemaVersionTable=version_schema 4 | functionsVersionTable=version_functions 5 | 6 | echo "- Applying schema migrations.." 7 | cd schema 8 | tern status --config $TERN_CONF --version-table $schemaVersionTable 9 | tern migrate --config $TERN_CONF --version-table $schemaVersionTable 10 | if [ $? -ne 0 ]; then exit 1; fi 11 | echo "Done" 12 | cd .. 13 | 14 | echo "- Loading functions.." 15 | cd functions 16 | tern status --config $TERN_CONF --version-table $functionsVersionTable | grep "version: 1 of 1" 17 | if [ $? -eq 0 ]; then 18 | tern migrate --config $TERN_CONF --version-table $functionsVersionTable --destination -+1 19 | else 20 | tern migrate --config $TERN_CONF --version-table $functionsVersionTable 21 | fi 22 | if [ $? -ne 0 ]; then exit 1; fi 23 | echo "Done" 24 | -------------------------------------------------------------------------------- /database/migrations/schema/0002_unique_application.sql: -------------------------------------------------------------------------------- 1 | alter table application add constraint application_job_seeker_profile_id_job_id_key unique (job_seeker_profile_id, job_id); 2 | 3 | ---- create above / drop below ---- 4 | 5 | alter table application drop constraint application_job_seeker_profile_id_job_id_key; 6 | -------------------------------------------------------------------------------- /database/migrations/schema/0003_job_tz_interval.sql: -------------------------------------------------------------------------------- 1 | alter table job add column tz_start text; 2 | alter table job add column tz_end text; 3 | 4 | ---- create above / drop below ---- 5 | 6 | alter table job drop column tz_start; 7 | alter table job drop column tz_end; 8 | -------------------------------------------------------------------------------- /database/migrations/schema/0004_salary_usd_year.sql: -------------------------------------------------------------------------------- 1 | alter table job add column salary_usd_year bigint check (salary_usd_year >= 0); 2 | alter table job add column salary_min_usd_year bigint check (salary_min_usd_year >= 0); 3 | alter table job add column salary_max_usd_year bigint check (salary_max_usd_year >= 0); 4 | 5 | ---- create above / drop below ---- 6 | 7 | alter table job drop column salary_usd_year; 8 | alter table job drop column salary_min_usd_year; 9 | alter table job drop column salary_max_usd_year; 10 | -------------------------------------------------------------------------------- /database/migrations/schema/0005_moderation.sql: -------------------------------------------------------------------------------- 1 | insert into job_status (name) values ('pending-approval'); 2 | insert into job_status (name) values ('rejected'); 3 | 4 | alter table "user" add moderator boolean not null default false; 5 | 6 | alter table job add review_notes text; 7 | alter table job add reviewed_by uuid references "user" (user_id) on delete set null; 8 | alter table job add reviewed_at timestamptz; 9 | 10 | create index job_reviewed_by_idx on job (reviewed_by); 11 | 12 | ---- create above / drop below ---- 13 | 14 | alter table job drop column review_notes; 15 | alter table job drop column reviewed_by; 16 | alter table job drop column reviewed_at; 17 | 18 | alter table "user" drop column moderator; 19 | 20 | delete from job_status where name = 'pending-approval'; 21 | delete from job_status where name = 'rejected'; 22 | -------------------------------------------------------------------------------- /database/migrations/schema/0006_profile_bluesky_url.sql: -------------------------------------------------------------------------------- 1 | alter table job_seeker_profile add column bluesky_url text check (bluesky_url <> ''); 2 | 3 | ---- create above / drop below ---- 4 | 5 | alter table job_seeker_profile drop column bluesky_url; 6 | -------------------------------------------------------------------------------- /database/migrations/schema/0007_foundation_landscape_url.sql: -------------------------------------------------------------------------------- 1 | alter table foundation add column landscape_url text check (landscape_url <> ''); 2 | 3 | ---- create above / drop below ---- 4 | 5 | alter table foundation drop column landscape_url; 6 | -------------------------------------------------------------------------------- /database/migrations/schema/0008_team_member_management.sql: -------------------------------------------------------------------------------- 1 | alter table employer_team add column approved boolean not null default false; 2 | update employer_team set approved = true; 3 | alter table employer_team add column created_at timestamptz default current_timestamp; 4 | 5 | alter table notification drop constraint notification_user_id_key; 6 | 7 | insert into notification_kind (name) values ('team-invitation'); 8 | 9 | ---- create above / drop below ---- 10 | 11 | alter table employer_team drop column approved; 12 | alter table employer_team drop column created_at; 13 | 14 | delete from notification_kind where name = 'team-invitation'; 15 | -------------------------------------------------------------------------------- /database/migrations/schema/0009_job_views.sql: -------------------------------------------------------------------------------- 1 | create table if not exists job_views ( 2 | job_id uuid references job on delete set null, 3 | day date not null, 4 | total integer not null, 5 | unique (job_id, day) 6 | ); 7 | 8 | ---- create above / drop below ---- 9 | 10 | drop table if exists job_views; 11 | -------------------------------------------------------------------------------- /database/migrations/schema/0010_first_published_at.sql: -------------------------------------------------------------------------------- 1 | alter table job add column first_published_at timestamptz; 2 | 3 | ---- create above / drop below ---- 4 | 5 | alter table job drop column first_published_at; 6 | -------------------------------------------------------------------------------- /database/migrations/schema/0011_soft_delete_job.sql: -------------------------------------------------------------------------------- 1 | insert into job_status (name) values ('deleted'); 2 | 3 | alter table job add column deleted_at timestamptz; 4 | 5 | ---- create above / drop below ---- 6 | 7 | delete from job_status where name = 'deleted'; 8 | 9 | alter table job drop column deleted_at; 10 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | # ABOUT 2 | 3 | **GitJobs** is a simple open source, developer first job board focused on: 4 | 5 | - open source job opportunities filterable by language, technology and foundation 6 | - highlighting the opportunities that contribute back to open source projects 7 | - encouraging sustainability by contributing back to upstream projects 8 | 9 | # FAQs 10 | 11 | ## How much does it cost to post a job? 12 | 13 | Posting your job listing is free! 14 | 15 | ## How long will my job listing be live? 16 | 17 | Your job listing will be live for 30 days. After that, it will be automatically archived and removed from the site. 18 | 19 | If your job is still open after that time, you can re-publish it again for another 30 days from the [employer dashboard](https://gitjobs.dev/dashboard/employer?tab=jobs). 20 | 21 | ## How do I edit a job listing or remove it from the site? 22 | 23 | You can manage your job listings from the [employer dashboard](https://gitjobs.dev/dashboard/employer?tab=jobs). Please note that it make take a few minutes for changes to be reflected on the site. 24 | 25 | ## What do the "Upstream Commitment" and "Open Source" bars mean? 26 | 27 | - The *Upstream Commitment* bar indicates how much time the employer provides to work on upstream open source projects the company depends on. 28 | 29 | - The *Open Source* bar indicates how much time of the job is allocated to developing open source code. 30 | 31 | Jobs postings that include "Upstream Commitment" or "Open Source" development time are featured on the site. 32 | 33 | ## Anything else? 34 | 35 | For any further questions, please contact us [by opening a discussion in the GitJobs repository](https://github.com/cncf/gitjobs/discussions). 36 | -------------------------------------------------------------------------------- /docs/screenshots/gitjobs1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cncf/gitjobs/7835dc18e436d262635d1b11f3caebcbc9685657/docs/screenshots/gitjobs1.png -------------------------------------------------------------------------------- /docs/screenshots/gitjobs2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cncf/gitjobs/7835dc18e436d262635d1b11f3caebcbc9685657/docs/screenshots/gitjobs2.png -------------------------------------------------------------------------------- /docs/screenshots/gitjobs3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cncf/gitjobs/7835dc18e436d262635d1b11f3caebcbc9685657/docs/screenshots/gitjobs3.png -------------------------------------------------------------------------------- /docs/screenshots/gitjobs4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cncf/gitjobs/7835dc18e436d262635d1b11f3caebcbc9685657/docs/screenshots/gitjobs4.png -------------------------------------------------------------------------------- /gitjobs-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gitjobs-server" 3 | description = "GitJobs HTTP server" 4 | version.workspace = true 5 | license.workspace = true 6 | edition.workspace = true 7 | rust-version.workspace = true 8 | 9 | [dependencies] 10 | anyhow = { workspace = true } 11 | askama = { workspace = true } 12 | async-trait = { workspace = true } 13 | axum = { workspace = true } 14 | axum-login = { workspace = true } 15 | axum-extra = { workspace = true } 16 | axum-messages = { workspace = true } 17 | cached = { workspace = true } 18 | clap = { workspace = true } 19 | chrono = { workspace = true } 20 | chrono-tz = { workspace = true } 21 | deadpool-postgres = { workspace = true } 22 | emojis = { workspace = true } 23 | figment = { workspace = true } 24 | human_format = { workspace = true } 25 | image = { workspace = true } 26 | lettre = { workspace = true } 27 | markdown = { workspace = true } 28 | mime_guess = { workspace = true } 29 | minify-html = { workspace = true } 30 | num-format = { workspace = true } 31 | oauth2 = { workspace = true } 32 | openidconnect = { workspace = true } 33 | openssl = { workspace = true } 34 | palette = { workspace = true } 35 | password-auth = { workspace = true } 36 | postgres-openssl = { workspace = true } 37 | rand = { workspace = true } 38 | regex = { workspace = true } 39 | reqwest = { workspace = true } 40 | rust-embed = { workspace = true } 41 | serde = { workspace = true } 42 | serde_html_form = { workspace = true } 43 | serde_json = { workspace = true } 44 | serde_qs = { workspace = true } 45 | serde_with = { workspace = true } 46 | strum = { workspace = true } 47 | thiserror = { workspace = true } 48 | time = { workspace = true } 49 | tokio = { workspace = true } 50 | tokio-postgres = { workspace = true } 51 | tower = { workspace = true } 52 | tower-http = { workspace = true } 53 | tower-sessions = { workspace = true } 54 | tokio-util = { workspace = true } 55 | tracing = { workspace = true } 56 | tracing-subscriber = { workspace = true } 57 | unicode-segmentation = { workspace = true } 58 | uuid = { workspace = true } 59 | 60 | [dev-dependencies] 61 | futures = { workspace = true } 62 | mockall = { workspace = true } 63 | 64 | [build-dependencies] 65 | anyhow = { workspace = true } 66 | which = { workspace = true } 67 | -------------------------------------------------------------------------------- /gitjobs-server/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build server 2 | FROM rust:1-alpine3.21 AS builder 3 | RUN apk --no-cache add musl-dev perl make 4 | RUN wget -O /usr/local/bin/tailwindcss https://github.com/tailwindlabs/tailwindcss/releases/download/v4.0.17/tailwindcss-linux-x64-musl 5 | RUN chmod +x /usr/local/bin/tailwindcss 6 | WORKDIR /gitjobs 7 | COPY Cargo.* ./ 8 | COPY docs/about.md docs/about.md 9 | COPY gitjobs-server gitjobs-server 10 | COPY gitjobs-syncer/Cargo.* gitjobs-syncer 11 | WORKDIR /gitjobs/gitjobs-server 12 | RUN cargo build --release 13 | 14 | # Final stage 15 | FROM alpine:3.21.3 16 | RUN apk --no-cache add ca-certificates && addgroup -S gitjobs && adduser -S gitjobs -G gitjobs 17 | USER gitjobs 18 | WORKDIR /home/gitjobs 19 | COPY --from=builder /gitjobs/target/release/gitjobs-server /usr/local/bin 20 | -------------------------------------------------------------------------------- /gitjobs-server/askama.toml: -------------------------------------------------------------------------------- 1 | [general] 2 | dirs = [ 3 | "templates" 4 | ] 5 | -------------------------------------------------------------------------------- /gitjobs-server/build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | use anyhow::{Result, bail}; 4 | use which::which; 5 | 6 | fn main() -> Result<()> { 7 | // Rerun this build script if changes are detected in the following paths. 8 | println!("cargo:rerun-if-changed=static"); 9 | println!("cargo:rerun-if-changed=templates"); 10 | 11 | // Check if required external tools are available 12 | if which("tailwindcss").is_err() { 13 | bail!("tailwindcss not found in PATH (required)"); 14 | } 15 | 16 | // Build styles 17 | run( 18 | "tailwindcss", 19 | &["-i", "static/css/styles.src.css", "-o", "static/css/styles.css"], 20 | )?; 21 | 22 | Ok(()) 23 | } 24 | 25 | /// Helper function to run a command. 26 | fn run(program: &str, args: &[&str]) -> Result<()> { 27 | // Setup command 28 | let mut cmd = new_cmd(program); 29 | cmd.args(args); 30 | 31 | // Execute it and check output 32 | let output = cmd.output()?; 33 | if !output.status.success() { 34 | bail!( 35 | "\n\n> {cmd:?} (stderr)\n{}\n> {cmd:?} (stdout)\n{}\n", 36 | String::from_utf8(output.stderr)?, 37 | String::from_utf8(output.stdout)? 38 | ); 39 | } 40 | 41 | Ok(()) 42 | } 43 | 44 | /// Helper function to setup a command based on the target OS. 45 | fn new_cmd(program: &str) -> Command { 46 | if cfg!(target_os = "windows") { 47 | let mut cmd = Command::new("cmd"); 48 | cmd.args(["/C", program]); 49 | cmd 50 | } else { 51 | Command::new(program) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /gitjobs-server/src/db/dashboard/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module defines some database functionality for the dashboards. 2 | 3 | use async_trait::async_trait; 4 | use employer::DBDashBoardEmployer; 5 | use job_seeker::DBDashBoardJobSeeker; 6 | use moderator::DBDashBoardModerator; 7 | 8 | use crate::PgDB; 9 | 10 | pub(crate) mod employer; 11 | pub(crate) mod job_seeker; 12 | pub(crate) mod moderator; 13 | 14 | /// Trait that defines database operations used in the dashboards. 15 | #[async_trait] 16 | pub(crate) trait DBDashBoard: 17 | DBDashBoardEmployer + DBDashBoardJobSeeker + DBDashBoardModerator 18 | { 19 | } 20 | 21 | impl DBDashBoard for PgDB {} 22 | -------------------------------------------------------------------------------- /gitjobs-server/src/db/img.rs: -------------------------------------------------------------------------------- 1 | //! This module defines database operations for managing images and their versions. 2 | 3 | use std::sync::Arc; 4 | 5 | use anyhow::Result; 6 | use async_trait::async_trait; 7 | use tracing::{instrument, trace}; 8 | use uuid::Uuid; 9 | 10 | use crate::{ 11 | PgDB, 12 | img::{ImageFormat, ImageVersion}, 13 | }; 14 | 15 | /// Trait for database operations related to image management. 16 | #[async_trait] 17 | pub(crate) trait DBImage { 18 | /// Retrieves a specific version of an image from the database. 19 | async fn get_image_version( 20 | &self, 21 | image_id: Uuid, 22 | version: &str, 23 | ) -> Result, ImageFormat)>>; 24 | 25 | /// Saves multiple image versions in the database. 26 | async fn save_image_versions(&self, user_id: &Uuid, versions: Vec) -> Result; 27 | } 28 | 29 | /// Shared pointer to a thread-safe, async `DBImage` trait object. 30 | pub(crate) type DynDBImage = Arc; 31 | 32 | /// Implementation of DBImage for the PgDB database backend. 33 | #[async_trait] 34 | impl DBImage for PgDB { 35 | #[instrument(skip(self), err)] 36 | async fn get_image_version( 37 | &self, 38 | image_id: Uuid, 39 | version: &str, 40 | ) -> Result, ImageFormat)>> { 41 | trace!("db: get image version"); 42 | 43 | let db = self.pool.get().await?; 44 | let Some(row) = db 45 | .query_opt( 46 | "select data, format from get_image_version($1::uuid, $2::text)", 47 | &[&image_id, &version], 48 | ) 49 | .await? 50 | else { 51 | return Ok(None); 52 | }; 53 | let data = row.get::<_, Vec>("data"); 54 | let format = ImageFormat::try_from(row.get::<_, &str>("format"))?; 55 | 56 | Ok(Some((data, format))) 57 | } 58 | 59 | #[instrument(skip(self, versions), err)] 60 | async fn save_image_versions(&self, user_id: &Uuid, versions: Vec) -> Result { 61 | trace!("db: save image versions"); 62 | 63 | // Begin transaction 64 | let mut db = self.pool.get().await?; 65 | let tx = db.transaction().await?; 66 | 67 | // Insert image identifier 68 | let image_id = Uuid::new_v4(); 69 | tx.execute( 70 | " 71 | insert into image ( 72 | image_id, 73 | created_by 74 | ) values ( 75 | $1::uuid, 76 | $2::uuid 77 | )", 78 | &[&image_id, &user_id], 79 | ) 80 | .await?; 81 | 82 | // Insert image versions 83 | for v in versions { 84 | tx.execute( 85 | " 86 | insert into image_version (image_id, version, data) 87 | values ($1::uuid, $2::text, $3::bytea) 88 | ", 89 | &[&image_id, &v.version, &v.data], 90 | ) 91 | .await?; 92 | } 93 | 94 | // Commit transaction 95 | tx.commit().await?; 96 | 97 | Ok(image_id) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /gitjobs-server/src/db/views.rs: -------------------------------------------------------------------------------- 1 | //! This module defines database functionality used in the views tracker, including 2 | //! operations for updating job view counts. 3 | 4 | use std::sync::Arc; 5 | 6 | use anyhow::Result; 7 | use async_trait::async_trait; 8 | #[cfg(test)] 9 | use mockall::automock; 10 | use tokio_postgres::types::Json; 11 | use tracing::{instrument, trace}; 12 | 13 | use crate::{ 14 | db::PgDB, 15 | views::{Day, JobId, Total}, 16 | }; 17 | 18 | /// Lock key used to synchronize updates to job views in the database. 19 | const LOCK_KEY_UPDATE_JOBS_VIEWS: i64 = 1; 20 | 21 | /// Trait that defines database operations used in the views tracker. 22 | #[async_trait] 23 | #[cfg_attr(test, automock)] 24 | pub(crate) trait DBViews { 25 | /// Updates the number of views for the provided jobs and days. 26 | async fn update_jobs_views(&self, data: Vec<(JobId, Day, Total)>) -> Result<()>; 27 | } 28 | 29 | /// Type alias for a thread-safe, reference-counted `DBViews` trait object. 30 | pub(crate) type DynDBViews = Arc; 31 | 32 | #[async_trait] 33 | impl DBViews for PgDB { 34 | #[instrument(skip(self), err)] 35 | async fn update_jobs_views(&self, data: Vec<(JobId, Day, Total)>) -> Result<()> { 36 | trace!("db: update jobs views"); 37 | 38 | let db = self.pool.get().await?; 39 | db.execute( 40 | "select update_jobs_views($1::bigint, $2::jsonb)", 41 | &[&LOCK_KEY_UPDATE_JOBS_VIEWS, &Json(&data)], 42 | ) 43 | .await?; 44 | 45 | Ok(()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /gitjobs-server/src/db/workers.rs: -------------------------------------------------------------------------------- 1 | //! This module defines database operations used by background task workers, such as 2 | //! archiving expired jobs. 3 | 4 | use anyhow::Result; 5 | use async_trait::async_trait; 6 | use tracing::instrument; 7 | 8 | use crate::db::PgDB; 9 | 10 | /// Trait for database operations required by background tasks workers. 11 | #[async_trait] 12 | pub(crate) trait DBWorkers { 13 | /// Archives jobs that have expired based on their published date. 14 | async fn archive_expired_jobs(&self) -> Result<()>; 15 | } 16 | 17 | #[async_trait] 18 | impl DBWorkers for PgDB { 19 | #[instrument(skip(self), err)] 20 | async fn archive_expired_jobs(&self) -> Result<()> { 21 | let db = self.pool.get().await?; 22 | db.execute( 23 | " 24 | update job 25 | set 26 | status = 'archived', 27 | archived_at = current_timestamp, 28 | updated_at = current_timestamp 29 | where status = 'published' 30 | and published_at + '30 days'::interval < current_timestamp; 31 | ", 32 | &[], 33 | ) 34 | .await?; 35 | 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/dashboard/employer/applications.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the HTTP handlers for the applications page. 2 | 3 | use anyhow::Result; 4 | use askama::Template; 5 | use axum::{ 6 | extract::{Path, State}, 7 | response::{Html, IntoResponse}, 8 | }; 9 | use reqwest::StatusCode; 10 | use serde_qs::axum::QsQuery; 11 | use tracing::instrument; 12 | use uuid::Uuid; 13 | 14 | use crate::{ 15 | db::{DynDB, dashboard::employer::ApplicationsSearchOutput}, 16 | handlers::{error::HandlerError, extractors::SelectedEmployerIdRequired}, 17 | templates::{ 18 | dashboard::{ 19 | employer::applications::{ApplicationsPage, Filters}, 20 | job_seeker, 21 | }, 22 | pagination::NavigationLinks, 23 | }, 24 | }; 25 | 26 | // Pages handlers. 27 | 28 | /// Renders the applications list page for the selected employer. 29 | #[instrument(skip_all, err)] 30 | pub(crate) async fn list_page( 31 | State(db): State, 32 | SelectedEmployerIdRequired(employer_id): SelectedEmployerIdRequired, 33 | QsQuery(filters): QsQuery, 34 | ) -> Result { 35 | // Get filter options and applications that match the query 36 | let (filters_options, ApplicationsSearchOutput { applications, total }) = tokio::try_join!( 37 | db.get_applications_filters_options(&employer_id), 38 | db.search_applications(&employer_id, &filters) 39 | )?; 40 | 41 | // Prepare template 42 | let navigation_links = NavigationLinks::from_filters(&filters, total)?; 43 | let template = ApplicationsPage { 44 | applications, 45 | filters, 46 | filters_options, 47 | navigation_links, 48 | }; 49 | 50 | Ok(Html(template.render()?)) 51 | } 52 | 53 | /// Renders the page to preview a job seeker's profile for employers. 54 | #[instrument(skip_all, err)] 55 | pub(crate) async fn profile_preview_page( 56 | State(db): State, 57 | Path(profile_id): Path, 58 | ) -> Result { 59 | let Some(user_id) = db.get_job_seeker_user_id(&profile_id).await? else { 60 | return Ok(StatusCode::NOT_FOUND.into_response()); 61 | }; 62 | let Some(profile) = db.get_job_seeker_profile(&user_id).await? else { 63 | return Ok(StatusCode::NOT_FOUND.into_response()); 64 | }; 65 | let template = job_seeker::profile::PreviewPage { profile }; 66 | 67 | Ok(Html(template.render()?).into_response()) 68 | } 69 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/dashboard/employer/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the HTTP handlers for the employer dashboard. 2 | 3 | pub(crate) mod applications; 4 | pub(crate) mod employers; 5 | pub(crate) mod home; 6 | pub(crate) mod jobs; 7 | pub(crate) mod team; 8 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/dashboard/job_seeker/applications.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the HTTP handlers for the applications page. 2 | 3 | use askama::Template; 4 | use axum::{ 5 | extract::{Path, State}, 6 | response::{Html, IntoResponse}, 7 | }; 8 | use reqwest::StatusCode; 9 | use tracing::instrument; 10 | use uuid::Uuid; 11 | 12 | use crate::{ 13 | auth::AuthSession, db::DynDB, handlers::error::HandlerError, 14 | templates::dashboard::job_seeker::applications::ApplicationsPage, 15 | }; 16 | 17 | // Pages handlers. 18 | 19 | /// Handler that returns the applications list page. 20 | #[instrument(skip_all, err)] 21 | pub(crate) async fn list_page( 22 | auth_session: AuthSession, 23 | State(db): State, 24 | ) -> Result { 25 | // Get user from session 26 | let Some(user) = auth_session.user else { 27 | return Ok(StatusCode::FORBIDDEN.into_response()); 28 | }; 29 | 30 | // Prepare template 31 | let applications = db.list_job_seeker_applications(&user.user_id).await?; 32 | let template = ApplicationsPage { applications }; 33 | 34 | Ok(Html(template.render()?).into_response()) 35 | } 36 | 37 | // Actions handlers. 38 | 39 | /// Handler that cancels an application. 40 | #[instrument(skip_all, err)] 41 | pub(crate) async fn cancel( 42 | auth_session: AuthSession, 43 | Path(application_id): Path, 44 | State(db): State, 45 | ) -> Result { 46 | // Get user from session 47 | let Some(user) = auth_session.user else { 48 | return Ok((StatusCode::FORBIDDEN).into_response()); 49 | }; 50 | 51 | // Cancel application 52 | db.cancel_application(&application_id, &user.user_id).await?; 53 | 54 | Ok(( 55 | StatusCode::NO_CONTENT, 56 | [( 57 | "HX-Location", 58 | r#"{"path":"/dashboard/job-seeker?tab=applications", "target":"body"}"#, 59 | )], 60 | ) 61 | .into_response()) 62 | } 63 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/dashboard/job_seeker/home.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the HTTP handlers for the job seeker dashboard home page. 2 | 3 | use std::collections::HashMap; 4 | 5 | use anyhow::Result; 6 | use askama::Template; 7 | use axum::{ 8 | extract::{Query, State}, 9 | http::StatusCode, 10 | response::{Html, IntoResponse}, 11 | }; 12 | use axum_messages::Messages; 13 | use tower_sessions::Session; 14 | use tracing::instrument; 15 | 16 | use crate::{ 17 | auth::AuthSession, 18 | config::HttpServerConfig, 19 | db::DynDB, 20 | handlers::{auth::AUTH_PROVIDER_KEY, error::HandlerError}, 21 | templates::{ 22 | PageId, auth, 23 | dashboard::job_seeker::{ 24 | applications, 25 | home::{self, Content, Tab}, 26 | profile, 27 | }, 28 | }, 29 | }; 30 | 31 | // Pages handlers. 32 | 33 | /// Handler that returns the job seeker dashboard home page. 34 | #[instrument(skip_all, err)] 35 | pub(crate) async fn page( 36 | auth_session: AuthSession, 37 | messages: Messages, 38 | session: Session, 39 | State(db): State, 40 | State(cfg): State, 41 | Query(query): Query>, 42 | ) -> Result { 43 | // Get user from session 44 | let Some(user) = auth_session.user.clone() else { 45 | return Ok(StatusCode::FORBIDDEN.into_response()); 46 | }; 47 | 48 | // Prepare content for the selected tab 49 | let tab: Tab = query.get("tab").unwrap_or(&String::new()).parse().unwrap_or_default(); 50 | let content = match tab { 51 | Tab::Account => { 52 | let user_summary = user.clone().into(); 53 | Content::Account(auth::UpdateUserPage { user_summary }) 54 | } 55 | Tab::Applications => { 56 | let applications = db.list_job_seeker_applications(&user.user_id).await?; 57 | Content::Applications(applications::ApplicationsPage { applications }) 58 | } 59 | Tab::Profile => { 60 | let profile = db.get_job_seeker_profile(&user.user_id).await?; 61 | Content::Profile(profile::UpdatePage { profile }) 62 | } 63 | }; 64 | 65 | // Prepare template 66 | let template = home::Page { 67 | auth_provider: session.get(AUTH_PROVIDER_KEY).await?, 68 | cfg: cfg.into(), 69 | content, 70 | messages: messages.into_iter().collect(), 71 | page_id: PageId::JobSeekerDashboard, 72 | user: auth_session.into(), 73 | }; 74 | 75 | Ok(Html(template.render()?).into_response()) 76 | } 77 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/dashboard/job_seeker/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the HTTP handlers for the job seeker dashboard. 2 | 3 | pub(crate) mod applications; 4 | pub(crate) mod home; 5 | pub(crate) mod profile; 6 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/dashboard/job_seeker/profile.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the HTTP handlers for previewing, updating, and saving job 2 | //! seeker profiles. 3 | 4 | use askama::Template; 5 | use axum::{ 6 | extract::State, 7 | response::{Html, IntoResponse}, 8 | }; 9 | use reqwest::StatusCode; 10 | use tracing::instrument; 11 | 12 | use crate::{ 13 | auth::AuthSession, 14 | db::DynDB, 15 | handlers::error::HandlerError, 16 | templates::dashboard::job_seeker::profile::{self, JobSeekerProfile}, 17 | }; 18 | 19 | // Pages handlers. 20 | 21 | /// Handler that returns the page to preview a profile. 22 | #[instrument(skip_all, err)] 23 | pub(crate) async fn preview_page( 24 | State(serde_qs_de): State, 25 | body: String, 26 | ) -> Result { 27 | // Get profile information from body 28 | let mut profile: JobSeekerProfile = match serde_qs_de.deserialize_str(&body).map_err(anyhow::Error::new) { 29 | Ok(profile) => profile, 30 | Err(e) => return Ok((StatusCode::UNPROCESSABLE_ENTITY, e.to_string()).into_response()), 31 | }; 32 | profile.normalize(); 33 | 34 | // Prepare template 35 | let template = profile::PreviewPage { profile }; 36 | 37 | Ok(Html(template.render()?).into_response()) 38 | } 39 | 40 | /// Handler that returns the page to update a profile. 41 | #[instrument(skip_all, err)] 42 | pub(crate) async fn update_page( 43 | auth_session: AuthSession, 44 | State(db): State, 45 | ) -> Result { 46 | // Get user from session 47 | let Some(user) = auth_session.user else { 48 | return Ok(StatusCode::FORBIDDEN.into_response()); 49 | }; 50 | 51 | // Prepare template 52 | let profile = db.get_job_seeker_profile(&user.user_id).await?; 53 | let template = profile::UpdatePage { profile }; 54 | 55 | Ok(Html(template.render()?).into_response()) 56 | } 57 | 58 | // Actions handlers. 59 | 60 | /// Handler that updates a job seeker's profile in the database. 61 | #[instrument(skip_all, err)] 62 | pub(crate) async fn update( 63 | State(db): State, 64 | State(serde_qs_de): State, 65 | auth_session: AuthSession, 66 | body: String, 67 | ) -> Result { 68 | // Get user from session 69 | let Some(user) = auth_session.user else { 70 | return Ok(StatusCode::FORBIDDEN.into_response()); 71 | }; 72 | 73 | // Get profile information from body 74 | let mut profile: JobSeekerProfile = match serde_qs_de.deserialize_str(&body).map_err(anyhow::Error::new) { 75 | Ok(profile) => profile, 76 | Err(e) => return Ok((StatusCode::UNPROCESSABLE_ENTITY, e.to_string()).into_response()), 77 | }; 78 | profile.normalize(); 79 | 80 | // Update profile in database 81 | db.update_job_seeker_profile(&user.user_id, &profile).await?; 82 | 83 | Ok(StatusCode::NO_CONTENT.into_response()) 84 | } 85 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/dashboard/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the HTTP handlers for the dashboards. 2 | 3 | pub(crate) mod employer; 4 | pub(crate) mod job_seeker; 5 | pub(crate) mod moderator; 6 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/dashboard/moderator/home.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the HTTP handlers for the moderator dashboard home page. 2 | 3 | use std::collections::HashMap; 4 | 5 | use anyhow::Result; 6 | use askama::Template; 7 | use axum::{ 8 | extract::{Query, State}, 9 | http::StatusCode, 10 | response::{Html, IntoResponse}, 11 | }; 12 | use axum_messages::Messages; 13 | use tower_sessions::Session; 14 | use tracing::instrument; 15 | 16 | use crate::{ 17 | auth::AuthSession, 18 | config::HttpServerConfig, 19 | db::DynDB, 20 | handlers::{auth::AUTH_PROVIDER_KEY, error::HandlerError}, 21 | templates::{ 22 | PageId, 23 | dashboard::{ 24 | employer::jobs::JobStatus, 25 | moderator::{ 26 | home::{self, Content, Tab}, 27 | jobs, 28 | }, 29 | }, 30 | }, 31 | }; 32 | 33 | // Pages handlers. 34 | 35 | /// Handler that returns the moderator dashboard home page. 36 | /// 37 | /// This function handles the HTTP request for the moderator dashboard home page. 38 | /// It retrieves the user from the session, determines the selected tab, fetches 39 | /// the relevant data from the database, and renders the appropriate template. 40 | #[instrument(skip_all, err)] 41 | pub(crate) async fn page( 42 | auth_session: AuthSession, 43 | messages: Messages, 44 | session: Session, 45 | State(db): State, 46 | State(cfg): State, 47 | Query(query): Query>, 48 | ) -> Result { 49 | // Get user from session 50 | let Some(_user) = auth_session.user.clone() else { 51 | return Ok(StatusCode::FORBIDDEN.into_response()); 52 | }; 53 | 54 | // Prepare content for the selected tab 55 | let tab: Tab = query.get("tab").unwrap_or(&String::new()).parse().unwrap_or_default(); 56 | let content = match tab { 57 | Tab::LiveJobs => { 58 | let jobs = db.list_jobs_for_moderation(JobStatus::Published).await?; 59 | Content::LiveJobs(jobs::LivePage { jobs }) 60 | } 61 | Tab::PendingJobs => { 62 | let jobs = db.list_jobs_for_moderation(JobStatus::PendingApproval).await?; 63 | Content::PendingJobs(jobs::PendingPage { jobs }) 64 | } 65 | }; 66 | 67 | // Prepare template 68 | let template = home::Page { 69 | auth_provider: session.get(AUTH_PROVIDER_KEY).await?, 70 | cfg: cfg.into(), 71 | content, 72 | messages: messages.into_iter().collect(), 73 | page_id: PageId::ModeratorDashboard, 74 | user: auth_session.into(), 75 | }; 76 | 77 | Ok(Html(template.render()?).into_response()) 78 | } 79 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/dashboard/moderator/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the HTTP handlers for the moderator dashboard. 2 | 3 | pub(crate) mod home; 4 | pub(crate) mod jobs; 5 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/error.rs: -------------------------------------------------------------------------------- 1 | //! This module defines a `HandlerError` type to make error propagation easier 2 | //! in handlers. 3 | 4 | use axum::{ 5 | http::StatusCode, 6 | response::{IntoResponse, Response}, 7 | }; 8 | 9 | /// Represents all possible errors that can occur in a handler. 10 | #[derive(thiserror::Error, Debug)] 11 | pub(crate) enum HandlerError { 12 | /// Error related to authentication, contains a message. 13 | #[error("auth error: {0}")] 14 | Auth(String), 15 | 16 | /// Error during JSON serialization or deserialization. 17 | #[error("serde json error: {0}")] 18 | Serde(#[from] serde_json::Error), 19 | 20 | /// Error related to session management. 21 | #[error("session error: {0}")] 22 | Session(#[from] tower_sessions::session::Error), 23 | 24 | /// Error during template rendering. 25 | #[error("template error: {0}")] 26 | Template(#[from] askama::Error), 27 | 28 | /// Any other error, wrapped in `anyhow::Error` for flexibility. 29 | #[error(transparent)] 30 | Other(#[from] anyhow::Error), 31 | } 32 | 33 | /// Enables conversion of `HandlerError` into an HTTP response for Axum handlers. 34 | impl IntoResponse for HandlerError { 35 | fn into_response(self) -> Response { 36 | StatusCode::INTERNAL_SERVER_ERROR.into_response() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/img.rs: -------------------------------------------------------------------------------- 1 | //! HTTP handlers for image management, including upload and retrieval. 2 | 3 | use axum::{ 4 | extract::{Multipart, Path, State}, 5 | http::{HeaderMap, HeaderValue}, 6 | response::IntoResponse, 7 | }; 8 | use reqwest::{ 9 | StatusCode, 10 | header::{CACHE_CONTROL, CONTENT_LENGTH, CONTENT_TYPE}, 11 | }; 12 | use tracing::instrument; 13 | use uuid::Uuid; 14 | 15 | use crate::{ 16 | auth::AuthSession, 17 | handlers::error::HandlerError, 18 | img::{DynImageStore, ImageFormat}, 19 | }; 20 | 21 | /// Returns an image from the store, setting headers for cache and content type. 22 | #[instrument(skip_all, err)] 23 | pub(crate) async fn get( 24 | State(image_store): State, 25 | Path((image_id, version)): Path<(Uuid, String)>, 26 | ) -> Result { 27 | // Get image from the store 28 | let Some((data, format)) = image_store.get(image_id, &version).await? else { 29 | return Ok(StatusCode::NOT_FOUND.into_response()); 30 | }; 31 | 32 | // Prepare response headers 33 | let mut headers = HeaderMap::new(); 34 | let content_type = match format { 35 | ImageFormat::Png => "image/png", 36 | ImageFormat::Svg => "image/svg+xml", 37 | }; 38 | headers.insert( 39 | CACHE_CONTROL, 40 | HeaderValue::from_static("max-age=2592000, immutable"), 41 | ); 42 | headers.insert(CONTENT_LENGTH, data.len().into()); 43 | headers.insert(CONTENT_TYPE, HeaderValue::from_static(content_type)); 44 | 45 | Ok((headers, data).into_response()) 46 | } 47 | 48 | /// Handles image upload from authenticated users, saving the image to the store. 49 | #[instrument(skip_all, err)] 50 | pub(crate) async fn upload( 51 | auth_session: AuthSession, 52 | State(image_store): State, 53 | mut multipart: Multipart, 54 | ) -> Result { 55 | // Get user from session 56 | let Some(user) = auth_session.user else { 57 | return Ok(StatusCode::FORBIDDEN.into_response()); 58 | }; 59 | 60 | // Get image file name and data from the multipart form data 61 | let (file_name, data) = if let Ok(Some(field)) = multipart.next_field().await { 62 | let file_name = field.file_name().unwrap_or_default().to_string(); 63 | let Ok(data) = field.bytes().await else { 64 | return Ok(StatusCode::BAD_REQUEST.into_response()); 65 | }; 66 | (file_name, data) 67 | } else { 68 | return Ok(StatusCode::BAD_REQUEST.into_response()); 69 | }; 70 | 71 | // Save image to store 72 | let image_id = image_store.save(&user.user_id, &file_name, data.to_vec()).await?; 73 | 74 | Ok((StatusCode::OK, image_id.to_string()).into_response()) 75 | } 76 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/jobboard/about.rs: -------------------------------------------------------------------------------- 1 | //! HTTP handlers for the about page. 2 | 3 | use anyhow::{Result, anyhow}; 4 | use askama::Template; 5 | use axum::{ 6 | extract::State, 7 | response::{Html, IntoResponse}, 8 | }; 9 | use cached::proc_macro::cached; 10 | use chrono::Duration; 11 | use tower_sessions::Session; 12 | use tracing::instrument; 13 | 14 | use crate::{ 15 | auth::AuthSession, 16 | config::HttpServerConfig, 17 | handlers::{auth::AUTH_PROVIDER_KEY, error::HandlerError, prepare_headers}, 18 | templates::{PageId, jobboard::about::Page}, 19 | }; 20 | 21 | /// Handler that returns the about page. 22 | #[instrument(skip_all, err)] 23 | pub(crate) async fn page( 24 | auth_session: AuthSession, 25 | session: Session, 26 | State(cfg): State, 27 | ) -> Result { 28 | // Prepare template 29 | let template = Page { 30 | auth_provider: session.get(AUTH_PROVIDER_KEY).await?, 31 | cfg: cfg.into(), 32 | content: prepare_content()?, 33 | page_id: PageId::About, 34 | user: auth_session.into(), 35 | }; 36 | 37 | // Prepare response headers 38 | let headers = prepare_headers(Duration::hours(1), &[])?; 39 | 40 | Ok((headers, Html(template.render()?))) 41 | } 42 | 43 | /// Prepares and caches the about page content as HTML from Markdown source. 44 | #[cached( 45 | key = "&str", 46 | convert = r#"{ "about_content" }"#, 47 | sync_writes = "by_key", 48 | result = true 49 | )] 50 | pub(crate) fn prepare_content() -> Result { 51 | let md = include_str!("../../../../docs/about.md"); 52 | let options = markdown::Options::gfm(); 53 | let html = markdown::to_html_with_options(md, &options).map_err(|e| anyhow!(e))?; 54 | Ok(html) 55 | } 56 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/jobboard/embed.rs: -------------------------------------------------------------------------------- 1 | //! HTTP handlers for job board embed endpoints, including jobs and job card embeds. 2 | 3 | use anyhow::Result; 4 | use askama::Template; 5 | use axum::{ 6 | extract::{Path, State}, 7 | response::{Html, IntoResponse}, 8 | }; 9 | use chrono::Duration; 10 | use serde_qs::axum::QsQuery; 11 | use tracing::instrument; 12 | use uuid::Uuid; 13 | 14 | use crate::{ 15 | config::HttpServerConfig, 16 | db::{DynDB, jobboard::JobsSearchOutput}, 17 | handlers::{error::HandlerError, prepare_headers}, 18 | templates::jobboard::{ 19 | embed::{JobCard, JobsPage}, 20 | jobs::Filters, 21 | }, 22 | }; 23 | 24 | /// Returns the jobs embed page for external integration. 25 | #[instrument(skip_all, err)] 26 | pub(crate) async fn jobs_page( 27 | State(cfg): State, 28 | State(db): State, 29 | QsQuery(filters): QsQuery, 30 | ) -> Result { 31 | // Get jobs that match the query 32 | let JobsSearchOutput { jobs, total: _ } = db.search_jobs(&filters).await?; 33 | 34 | // Prepare template 35 | let template = JobsPage { 36 | base_url: cfg.base_url.strip_suffix('/').unwrap_or(&cfg.base_url).to_string(), 37 | jobs, 38 | }; 39 | 40 | // Prepare response headers 41 | let headers = prepare_headers(Duration::minutes(10), &[])?; 42 | 43 | Ok((headers, Html(template.render()?))) 44 | } 45 | 46 | /// Returns the job card embed as an SVG image for sharing or embedding. 47 | #[instrument(skip_all, err)] 48 | pub(crate) async fn job_card( 49 | State(cfg): State, 50 | State(db): State, 51 | Path(job_id): Path, 52 | ) -> Result { 53 | // Prepare template 54 | let template = JobCard { 55 | base_url: cfg.base_url.strip_suffix('/').unwrap_or(&cfg.base_url).to_string(), 56 | job: db.get_job_jobboard(&job_id).await?, 57 | }; 58 | 59 | // Prepare response headers 60 | let extra_headers = [("content-type", "image/svg+xml")]; 61 | let headers = prepare_headers(Duration::minutes(10), &extra_headers)?; 62 | 63 | Ok((headers, template.render()?)) 64 | } 65 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/jobboard/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the HTTP handlers for the job board. 2 | 3 | pub(crate) mod about; 4 | pub(crate) mod embed; 5 | pub(crate) mod jobs; 6 | pub(crate) mod stats; 7 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/jobboard/stats.rs: -------------------------------------------------------------------------------- 1 | //! HTTP handlers for the stats page. 2 | 3 | use anyhow::Result; 4 | use askama::Template; 5 | use axum::{ 6 | extract::State, 7 | response::{Html, IntoResponse}, 8 | }; 9 | use chrono::Duration; 10 | use tower_sessions::Session; 11 | use tracing::instrument; 12 | 13 | use crate::{ 14 | auth::AuthSession, 15 | config::HttpServerConfig, 16 | db::DynDB, 17 | handlers::{auth::AUTH_PROVIDER_KEY, error::HandlerError, prepare_headers}, 18 | templates::{PageId, jobboard::stats::Page}, 19 | }; 20 | 21 | /// Handler that returns the stats page. 22 | #[instrument(skip_all, err)] 23 | pub(crate) async fn page( 24 | auth_session: AuthSession, 25 | session: Session, 26 | State(cfg): State, 27 | State(db): State, 28 | ) -> Result { 29 | // Get stats information from the database 30 | let stats = db.get_stats().await?; 31 | 32 | // Prepare template 33 | let template = Page { 34 | auth_provider: session.get(AUTH_PROVIDER_KEY).await?, 35 | cfg: cfg.into(), 36 | page_id: PageId::Stats, 37 | stats, 38 | user: auth_session.into(), 39 | }; 40 | 41 | // Prepare response headers 42 | let headers = prepare_headers(Duration::hours(1), &[])?; 43 | 44 | Ok((headers, Html(template.render()?))) 45 | } 46 | -------------------------------------------------------------------------------- /gitjobs-server/src/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the HTTP handlers. 2 | 3 | use anyhow::Result; 4 | use axum::http::{HeaderMap, HeaderName, HeaderValue}; 5 | use chrono::Duration; 6 | use reqwest::header::CACHE_CONTROL; 7 | 8 | /// Authentication-related HTTP handlers. 9 | pub(crate) mod auth; 10 | /// Dashboard-related HTTP handlers. 11 | pub(crate) mod dashboard; 12 | /// Error handling utilities for HTTP handlers. 13 | pub(crate) mod error; 14 | /// Custom extractors for HTTP handlers. 15 | pub(crate) mod extractors; 16 | /// Image-related HTTP handlers. 17 | pub(crate) mod img; 18 | /// Job board HTTP handlers. 19 | pub(crate) mod jobboard; 20 | /// Miscellaneous HTTP handlers. 21 | pub(crate) mod misc; 22 | 23 | /// Helper function to prepare headers for HTTP responses, including cache control and 24 | /// additional custom headers. 25 | #[allow(unused_variables)] 26 | pub(crate) fn prepare_headers(cache_duration: Duration, extra_headers: &[(&str, &str)]) -> Result { 27 | let mut headers = HeaderMap::new(); 28 | 29 | // Set cache control header 30 | #[cfg(debug_assertions)] 31 | let duration_secs = 0; // Disable caching in debug mode 32 | #[cfg(not(debug_assertions))] 33 | let duration_secs = cache_duration.num_seconds(); 34 | headers.insert( 35 | CACHE_CONTROL, 36 | HeaderValue::try_from(format!("max-age={duration_secs}"))?, 37 | ); 38 | 39 | // Set extra headers 40 | for (key, value) in extra_headers { 41 | headers.insert(HeaderName::try_from(*key)?, HeaderValue::try_from(*value)?); 42 | } 43 | 44 | Ok(headers) 45 | } 46 | -------------------------------------------------------------------------------- /gitjobs-server/src/img/db.rs: -------------------------------------------------------------------------------- 1 | //! This module implements a database-backed image store. 2 | 3 | use anyhow::Result; 4 | use async_trait::async_trait; 5 | use uuid::Uuid; 6 | 7 | use crate::{ 8 | db::img::DynDBImage, 9 | img::{ImageFormat, ImageStore, ImageVersion, generate_versions, is_svg}, 10 | }; 11 | 12 | /// Database-backed image store implementation. 13 | pub(crate) struct DbImageStore { 14 | /// Database image interface for storing and retrieving images. 15 | db: DynDBImage, 16 | } 17 | 18 | impl DbImageStore { 19 | /// Create a new `DbImageStore` instance. 20 | pub(crate) fn new(db: DynDBImage) -> Self { 21 | Self { db } 22 | } 23 | } 24 | 25 | #[async_trait] 26 | impl ImageStore for DbImageStore { 27 | /// Retrieve an image version by its ID and version name. 28 | async fn get(&self, image_id: Uuid, version: &str) -> Result, ImageFormat)>> { 29 | self.db.get_image_version(image_id, version).await 30 | } 31 | 32 | /// Save an image and its generated versions to the database. 33 | async fn save(&self, user_id: &Uuid, filename: &str, data: Vec) -> Result { 34 | // Prepare image versions 35 | let versions = if is_svg(filename) { 36 | // Use the original svg image, no need to generate other versions 37 | vec![ImageVersion { 38 | data, 39 | version: "svg".to_string(), 40 | }] 41 | } else { 42 | // Generate versions for different sizes in png format 43 | tokio::task::spawn_blocking(move || generate_versions(&data)).await?? 44 | }; 45 | 46 | // Save image versions to the database 47 | self.db.save_image_versions(user_id, versions).await 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /gitjobs-server/src/img/mod.rs: -------------------------------------------------------------------------------- 1 | //! Types and functionality for managing image storage, formats, and processing. 2 | 3 | use std::{io::Cursor, sync::Arc}; 4 | 5 | use anyhow::Result; 6 | use async_trait::async_trait; 7 | use uuid::Uuid; 8 | 9 | pub(crate) mod db; 10 | 11 | /// Trait for image storage backends supporting get and save operations. 12 | #[async_trait] 13 | pub(crate) trait ImageStore { 14 | /// Retrieve an image version from the store. 15 | async fn get(&self, image_id: Uuid, version: &str) -> Result, ImageFormat)>>; 16 | 17 | /// Save an image to the store and return its unique identifier. 18 | async fn save(&self, user_id: &Uuid, filename: &str, data: Vec) -> Result; 19 | } 20 | 21 | /// Thread-safe trait object alias for image storage implementations. 22 | pub(crate) type DynImageStore = Arc; 23 | 24 | /// Generate resized versions of an image for multiple predefined sizes. 25 | pub(crate) fn generate_versions(data: &[u8]) -> Result> { 26 | // Read image data 27 | let img = image::ImageReader::new(Cursor::new(data)) 28 | .with_guessed_format()? 29 | .decode()?; 30 | 31 | // Generate versions for different sizes 32 | let mut versions = vec![]; 33 | for (size_name, size) in &[("small", 100), ("medium", 200), ("large", 400)] { 34 | // Resize image 35 | let version = img.resize(*size, *size, image::imageops::FilterType::Lanczos3); 36 | 37 | // Encode resized version of the image to png format 38 | let mut buf = vec![]; 39 | version.write_to(&mut Cursor::new(&mut buf), image::ImageFormat::Png)?; 40 | 41 | versions.push(ImageVersion { 42 | data: buf, 43 | version: (*size_name).to_string(), 44 | }); 45 | } 46 | 47 | Ok(versions) 48 | } 49 | 50 | /// Represents a version of an image of a specific size (or format). 51 | #[derive(Debug, Clone)] 52 | pub(crate) struct ImageVersion { 53 | /// Raw image data in the specified format. 54 | pub data: Vec, 55 | /// Version label, e.g., "small", "medium", or "large". 56 | pub version: String, 57 | } 58 | 59 | /// Supported image formats for storage and processing. 60 | #[derive(Debug, Clone, strum::Display, strum::EnumString)] 61 | #[strum(serialize_all = "lowercase")] 62 | pub(crate) enum ImageFormat { 63 | /// PNG image format. 64 | Png, 65 | /// SVG image format. 66 | Svg, 67 | } 68 | 69 | /// Returns true if the file name has an SVG extension (case-insensitive). 70 | pub(crate) fn is_svg(file_name: &str) -> bool { 71 | if let Some(extension) = file_name.split('.').next_back() { 72 | if extension.to_lowercase() == "svg" { 73 | return true; 74 | } 75 | } 76 | false 77 | } 78 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/dashboard/employer/employers.rs: -------------------------------------------------------------------------------- 1 | //! Templates and types for managing employers in the employer dashboard. 2 | 3 | use askama::Template; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_with::skip_serializing_none; 6 | use uuid::Uuid; 7 | 8 | use crate::templates::{ 9 | filters, 10 | helpers::build_dashboard_image_url, 11 | misc::{Foundation, Location, Member}, 12 | }; 13 | 14 | // Pages templates. 15 | 16 | /// Add employer page template. 17 | #[derive(Debug, Clone, Template, Serialize, Deserialize)] 18 | #[template(path = "dashboard/employer/employers/add.html")] 19 | pub(crate) struct AddPage { 20 | /// List of available foundations for employer association. 21 | pub foundations: Vec, 22 | } 23 | 24 | /// Employer initial setup page template. 25 | #[derive(Debug, Clone, Template, Serialize, Deserialize)] 26 | #[template(path = "dashboard/employer/employers/initial_setup.html")] 27 | pub(crate) struct InitialSetupPage {} 28 | 29 | /// Update employer page template. 30 | #[derive(Debug, Clone, Template, Serialize, Deserialize)] 31 | #[template(path = "dashboard/employer/employers/update.html")] 32 | pub(crate) struct UpdatePage { 33 | /// Employer details to update. 34 | pub employer: Employer, 35 | /// List of available foundations for employer association. 36 | pub foundations: Vec, 37 | } 38 | 39 | // Types. 40 | 41 | /// Employer summary information for dashboard listings. 42 | #[skip_serializing_none] 43 | #[derive(Debug, Clone, Serialize, Deserialize)] 44 | pub(crate) struct EmployerSummary { 45 | /// Unique identifier for the employer. 46 | pub employer_id: Uuid, 47 | /// Company name. 48 | pub company: String, 49 | 50 | /// Logo image identifier, if available. 51 | pub logo_id: Option, 52 | } 53 | 54 | /// Employer details for dashboard management. 55 | #[skip_serializing_none] 56 | #[derive(Debug, Clone, Serialize, Deserialize)] 57 | pub(crate) struct Employer { 58 | /// Company name. 59 | pub company: String, 60 | /// Company description. 61 | pub description: String, 62 | /// Whether the employer profile is public. 63 | pub public: bool, 64 | 65 | /// Location of the employer, if specified. 66 | pub location: Option, 67 | /// Logo image identifier, if available. 68 | pub logo_id: Option, 69 | /// Associated member information, if any. 70 | pub member: Option, 71 | /// Website URL, if provided. 72 | pub website_url: Option, 73 | } 74 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/dashboard/employer/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the templates for the employer dashboard. 2 | 3 | pub(crate) mod applications; 4 | pub(crate) mod employers; 5 | pub(crate) mod home; 6 | pub(crate) mod jobs; 7 | pub(crate) mod team; 8 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/dashboard/employer/team.rs: -------------------------------------------------------------------------------- 1 | //! Templates and types for the employer dashboard team page. 2 | 3 | use askama::Template; 4 | use chrono::{DateTime, Utc}; 5 | use serde::{Deserialize, Serialize}; 6 | use uuid::Uuid; 7 | 8 | use crate::templates::helpers::DATE_FORMAT; 9 | 10 | // Pages templates. 11 | 12 | /// Template for the team members list page. 13 | #[derive(Debug, Clone, Template, Serialize, Deserialize)] 14 | #[template(path = "dashboard/employer/teams/members_list.html")] 15 | pub(crate) struct MembersListPage { 16 | /// Count of approved team members. 17 | pub approved_members_count: usize, 18 | /// List of team members. 19 | pub members: Vec, 20 | } 21 | 22 | /// Template for the user invitations list page. 23 | #[derive(Debug, Clone, Template, Serialize, Deserialize)] 24 | #[template(path = "dashboard/employer/teams/invitations_list.html")] 25 | pub(crate) struct UserInvitationsListPage { 26 | /// List of team invitations. 27 | pub invitations: Vec, 28 | } 29 | 30 | // Types. 31 | 32 | /// Information about a team invitation. 33 | #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] 34 | pub(crate) struct TeamInvitation { 35 | /// Name of the company. 36 | pub company: String, 37 | /// Timestamp when the invitation was created. 38 | pub created_at: DateTime, 39 | /// ID of the employer who sent the invitation. 40 | pub employer_id: Uuid, 41 | } 42 | 43 | /// Information about a team member. 44 | #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] 45 | pub(crate) struct TeamMember { 46 | /// Whether the member is approved. 47 | pub approved: bool, 48 | /// Email address of the member. 49 | pub email: String, 50 | /// Full name of the member. 51 | pub name: String, 52 | /// Unique ID of the user. 53 | pub user_id: Uuid, 54 | /// Username of the member. 55 | pub username: String, 56 | } 57 | 58 | /// Information for adding a new team member. 59 | #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] 60 | pub(crate) struct NewTeamMember { 61 | /// Email address of the new member. 62 | pub email: String, 63 | } 64 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/dashboard/job_seeker/applications.rs: -------------------------------------------------------------------------------- 1 | //! Templates and types for the job seeker applications page. 2 | 3 | use askama::Template; 4 | use chrono::{DateTime, Utc}; 5 | use serde::{Deserialize, Serialize}; 6 | use uuid::Uuid; 7 | 8 | use crate::templates::{ 9 | dashboard::employer::jobs::{JobStatus, Workplace}, 10 | helpers::DATE_FORMAT, 11 | misc::Location, 12 | }; 13 | 14 | // Pages templates. 15 | 16 | /// Applications page template for job seeker dashboard. 17 | #[derive(Debug, Clone, Template, Serialize, Deserialize)] 18 | #[template(path = "dashboard/job_seeker/applications/list.html")] 19 | pub(crate) struct ApplicationsPage { 20 | /// List of job applications for the job seeker. 21 | pub applications: Vec, 22 | } 23 | 24 | // Types. 25 | 26 | /// Represents a job application entry for the job seeker dashboard. 27 | #[derive(Debug, Clone, Serialize, Deserialize)] 28 | pub(crate) struct Application { 29 | /// Unique identifier for the application. 30 | pub application_id: Uuid, 31 | /// Timestamp when the application was submitted. 32 | pub applied_at: DateTime, 33 | /// Unique identifier for the job. 34 | pub job_id: Uuid, 35 | /// Status of the job. 36 | pub job_status: JobStatus, 37 | /// Title of the job applied for. 38 | pub job_title: String, 39 | /// Workplace type for the job. 40 | pub job_workplace: Workplace, 41 | 42 | /// Location of the job, if specified. 43 | pub job_location: Option, 44 | } 45 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/dashboard/job_seeker/home.rs: -------------------------------------------------------------------------------- 1 | //! Templates and types for the job seeker dashboard home page. 2 | 3 | use askama::Template; 4 | use axum_messages::{Level, Message}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::templates::{ 8 | Config, PageId, 9 | auth::{self, User}, 10 | dashboard::job_seeker, 11 | filters, 12 | }; 13 | 14 | // Pages templates. 15 | 16 | /// Home page template for the job seeker dashboard. 17 | #[derive(Debug, Clone, Template)] 18 | #[template(path = "dashboard/job_seeker/home.html")] 19 | pub(crate) struct Page { 20 | /// Server configuration. 21 | pub cfg: Config, 22 | /// Content section for the dashboard. 23 | pub content: Content, 24 | /// Identifier for the current page. 25 | pub page_id: PageId, 26 | /// Flash or status messages to display. 27 | pub messages: Vec, 28 | /// Authenticated user information. 29 | pub user: User, 30 | 31 | /// Name of the authentication provider, if any. 32 | pub auth_provider: Option, 33 | } 34 | 35 | // Types. 36 | 37 | /// Content section for the job seeker dashboard home page. 38 | #[derive(Debug, Clone, Serialize, Deserialize)] 39 | #[allow(clippy::large_enum_variant)] 40 | pub(crate) enum Content { 41 | /// Account update page content. 42 | Account(auth::UpdateUserPage), 43 | /// Applications list page content. 44 | Applications(job_seeker::applications::ApplicationsPage), 45 | /// Profile update page content. 46 | Profile(job_seeker::profile::UpdatePage), 47 | } 48 | 49 | impl Content { 50 | /// Check if the content is the account page. 51 | fn is_account(&self) -> bool { 52 | matches!(self, Content::Account(_)) 53 | } 54 | 55 | /// Check if the content is the applications page. 56 | #[allow(dead_code)] 57 | fn is_applications(&self) -> bool { 58 | matches!(self, Content::Applications(_)) 59 | } 60 | 61 | /// Check if the content is the profile page. 62 | fn is_profile(&self) -> bool { 63 | matches!(self, Content::Profile(_)) 64 | } 65 | } 66 | 67 | impl std::fmt::Display for Content { 68 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 69 | match self { 70 | Content::Account(template) => write!(f, "{}", template.render()?), 71 | Content::Applications(template) => write!(f, "{}", template.render()?), 72 | Content::Profile(template) => write!(f, "{}", template.render()?), 73 | } 74 | } 75 | } 76 | 77 | /// Tab selection for the job seeker dashboard home page. 78 | #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, strum::Display, strum::EnumString)] 79 | #[strum(serialize_all = "kebab-case")] 80 | pub(crate) enum Tab { 81 | /// Account tab. 82 | Account, 83 | /// Applications tab. 84 | Applications, 85 | /// Profile tab (default). 86 | #[default] 87 | Profile, 88 | } 89 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/dashboard/job_seeker/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the templates for the job seeker dashboard. 2 | 3 | pub(crate) mod applications; 4 | pub(crate) mod home; 5 | pub(crate) mod profile; 6 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/dashboard/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the templates for the dashboard pages. 2 | 3 | pub(crate) mod employer; 4 | pub(crate) mod job_seeker; 5 | pub(crate) mod moderator; 6 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/dashboard/moderator/home.rs: -------------------------------------------------------------------------------- 1 | //! Templates and types for the moderator dashboard home page. 2 | 3 | use askama::Template; 4 | use axum_messages::{Level, Message}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::templates::{Config, PageId, auth::User, dashboard::moderator::jobs, filters}; 8 | 9 | // Pages templates. 10 | 11 | /// Template for the moderator dashboard home page. 12 | #[derive(Debug, Clone, Template)] 13 | #[template(path = "dashboard/moderator/home.html")] 14 | pub(crate) struct Page { 15 | /// Server configuration. 16 | pub cfg: Config, 17 | /// Content section for the dashboard. 18 | pub content: Content, 19 | /// Identifier for the current page. 20 | pub page_id: PageId, 21 | /// Flash or status messages to display. 22 | pub messages: Vec, 23 | /// Authenticated user information. 24 | pub user: User, 25 | 26 | /// Name of the authentication provider, if any. 27 | pub auth_provider: Option, 28 | } 29 | 30 | // Types. 31 | 32 | /// Content section for the moderator dashboard home page. 33 | #[derive(Debug, Clone, Serialize, Deserialize)] 34 | pub(crate) enum Content { 35 | /// Live jobs page content. 36 | LiveJobs(jobs::LivePage), 37 | /// Pending jobs page content. 38 | PendingJobs(jobs::PendingPage), 39 | } 40 | 41 | impl Content { 42 | /// Check if the content is the live jobs page. 43 | fn is_live_jobs(&self) -> bool { 44 | matches!(self, Content::LiveJobs(_)) 45 | } 46 | 47 | /// Check if the content is the pending jobs page. 48 | fn is_pending_jobs(&self) -> bool { 49 | matches!(self, Content::PendingJobs(_)) 50 | } 51 | } 52 | 53 | impl std::fmt::Display for Content { 54 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 55 | match self { 56 | Content::LiveJobs(template) => write!(f, "{}", template.render()?), 57 | Content::PendingJobs(template) => write!(f, "{}", template.render()?), 58 | } 59 | } 60 | } 61 | 62 | /// Tab selection for the moderator dashboard home page. 63 | #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, strum::Display, strum::EnumString)] 64 | #[strum(serialize_all = "kebab-case")] 65 | pub(crate) enum Tab { 66 | /// Live jobs tab. 67 | LiveJobs, 68 | /// Pending jobs tab (default). 69 | #[default] 70 | PendingJobs, 71 | } 72 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/dashboard/moderator/jobs.rs: -------------------------------------------------------------------------------- 1 | //! Templates and types for moderator dashboard jobs pages. 2 | 3 | use askama::Template; 4 | use chrono::{DateTime, Utc}; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_with::skip_serializing_none; 7 | use uuid::Uuid; 8 | 9 | use crate::templates::{ 10 | helpers::{DATE_FORMAT, DATE_FORMAT_3}, 11 | misc::Member, 12 | }; 13 | 14 | // Pages templates. 15 | 16 | /// Template for the live jobs page in the moderator dashboard. 17 | #[derive(Debug, Clone, Template, Serialize, Deserialize)] 18 | #[template(path = "dashboard/moderator/live_jobs.html")] 19 | pub(crate) struct LivePage { 20 | /// List of live jobs. 21 | pub jobs: Vec, 22 | } 23 | 24 | /// Template for the pending jobs page in the moderator dashboard. 25 | #[derive(Debug, Clone, Template, Serialize, Deserialize)] 26 | #[template(path = "dashboard/moderator/pending_jobs.html")] 27 | pub(crate) struct PendingPage { 28 | /// List of pending jobs. 29 | pub jobs: Vec, 30 | } 31 | 32 | // Types. 33 | 34 | /// Summary information for a job, used in moderator dashboard listings. 35 | #[skip_serializing_none] 36 | #[derive(Debug, Clone, Serialize, Deserialize)] 37 | pub(crate) struct JobSummary { 38 | /// Timestamp when the job was created. 39 | pub created_at: DateTime, 40 | /// Employer information for the job. 41 | pub employer: Employer, 42 | /// Unique identifier for the job. 43 | pub job_id: uuid::Uuid, 44 | /// Title of the job. 45 | pub title: String, 46 | } 47 | 48 | /// Employer information for job summaries in the moderator dashboard. 49 | #[skip_serializing_none] 50 | #[derive(Debug, Clone, Serialize, Deserialize)] 51 | pub(crate) struct Employer { 52 | /// Name of the company. 53 | pub company: String, 54 | /// Unique identifier for the employer. 55 | pub employer_id: Uuid, 56 | 57 | /// Optional logo identifier for the employer. 58 | pub logo_id: Option, 59 | /// Optional member information for the employer. 60 | pub member: Option, 61 | /// Optional website URL for the employer. 62 | pub website_url: Option, 63 | } 64 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/dashboard/moderator/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the templates for the moderator dashboard. 2 | 3 | pub(crate) mod home; 4 | pub(crate) mod jobs; 5 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/jobboard/about.rs: -------------------------------------------------------------------------------- 1 | //! Templates and types for the job board about page. 2 | 3 | use askama::Template; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::templates::{Config, PageId, auth::User, filters}; 7 | 8 | // Pages templates. 9 | 10 | /// Template for the about page. 11 | #[derive(Debug, Clone, Template, Serialize, Deserialize)] 12 | #[template(path = "jobboard/about/page.html")] 13 | pub(crate) struct Page { 14 | /// Server configuration. 15 | pub cfg: Config, 16 | /// About page content (rendered from markdown source). 17 | pub content: String, 18 | /// Identifier for the current page. 19 | pub page_id: PageId, 20 | /// Authenticated user information. 21 | pub user: User, 22 | 23 | /// Name of the authentication provider, if any. 24 | pub auth_provider: Option, 25 | } 26 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/jobboard/embed.rs: -------------------------------------------------------------------------------- 1 | //! Templates and types for job board embed pages and cards. 2 | 3 | use askama::Template; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::templates::{ 7 | dashboard::employer::jobs::{JobKind, Workplace}, 8 | filters, 9 | helpers::{DATE_FORMAT_3, build_jobboard_image_url}, 10 | jobboard::jobs::{Job, JobSummary}, 11 | }; 12 | 13 | /// Template for the jobs page embed, showing a list of job summaries. 14 | #[derive(Debug, Clone, Template, Serialize, Deserialize)] 15 | #[template(path = "jobboard/embed/jobs_page.html")] 16 | pub(crate) struct JobsPage { 17 | /// Base URL for job links and assets. 18 | pub base_url: String, 19 | /// List of jobs to display. 20 | pub jobs: Vec, 21 | } 22 | 23 | /// Template for a single job card embed, rendered as SVG. 24 | #[derive(Debug, Clone, Template, Serialize, Deserialize)] 25 | #[template(path = "jobboard/embed/job_card.svg")] 26 | pub(crate) struct JobCard { 27 | /// Base URL for job links. 28 | pub base_url: String, 29 | 30 | /// Job data to display in the card. 31 | pub job: Option, 32 | } 33 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/jobboard/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module defines the templates for the job board pages. 2 | 3 | pub(crate) mod about; 4 | pub(crate) mod embed; 5 | pub(crate) mod jobs; 6 | pub(crate) mod stats; 7 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/jobboard/stats.rs: -------------------------------------------------------------------------------- 1 | //! Templates and types for the job board about page. 2 | 3 | use askama::Template; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::templates::{Config, PageId, auth::User, filters}; 7 | 8 | // Pages templates. 9 | 10 | /// Template for the stats page. 11 | #[derive(Debug, Clone, Template, Serialize, Deserialize)] 12 | #[template(path = "jobboard/stats/page.html")] 13 | pub(crate) struct Page { 14 | /// Server configuration. 15 | pub cfg: Config, 16 | /// Identifier for the current page. 17 | pub page_id: PageId, 18 | /// Stats information in JSON format. 19 | pub stats: Stats, 20 | /// Authenticated user information. 21 | pub user: User, 22 | 23 | /// Name of the authentication provider, if any. 24 | pub auth_provider: Option, 25 | } 26 | 27 | // Types. 28 | 29 | /// Stats information. 30 | #[derive(Debug, Clone, Serialize, Deserialize)] 31 | pub(crate) struct Stats { 32 | /// Jobs statistics. 33 | pub jobs: JobsStats, 34 | /// Timestamp representing the current time. 35 | pub ts_now: Timestamp, 36 | /// Timestamp representing one month ago. 37 | pub ts_one_month_ago: Timestamp, 38 | /// Timestamp representing two years ago. 39 | pub ts_two_years_ago: Timestamp, 40 | } 41 | 42 | /// Jobs statistics. 43 | #[derive(Debug, Clone, Serialize, Deserialize)] 44 | pub(crate) struct JobsStats { 45 | /// Number of jobs published per foundation. 46 | /// Each entry is a tuple of (foundation, count). 47 | pub published_per_foundation: Option>, 48 | 49 | /// Number of jobs published per month. 50 | /// Each entry is a tuple of (year, month, count). 51 | pub published_per_month: Option>, 52 | 53 | /// Running total of published jobs. 54 | /// Each entry is a tuple of (timestamp, count). 55 | pub published_running_total: Option>, 56 | 57 | /// Number of job views per day. 58 | /// Each entry is a tuple of (timestamp, count). 59 | pub views_daily: Option>, 60 | 61 | /// Number of job views per month. 62 | /// Each entry is a tuple of (timestamp, count). 63 | pub views_monthly: Option>, 64 | } 65 | 66 | /// Type alias for a month. 67 | type Month = String; 68 | 69 | /// Type alias for a timestamp. 70 | type Timestamp = u64; 71 | 72 | /// Type alias for a total count. 73 | type Total = u64; 74 | 75 | /// Type alias for a year. 76 | type Year = String; 77 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module defines templates for rendering various job board components. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::config::{AnalyticsConfig, HttpServerConfig}; 6 | 7 | pub(crate) mod auth; 8 | pub(crate) mod dashboard; 9 | pub(crate) mod filters; 10 | pub(crate) mod helpers; 11 | pub(crate) mod jobboard; 12 | pub(crate) mod misc; 13 | pub(crate) mod notifications; 14 | pub(crate) mod pagination; 15 | 16 | /// Subset of the server configuration used in templates. 17 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 18 | pub(crate) struct Config { 19 | pub analytics: Option, 20 | } 21 | 22 | impl From for Config { 23 | fn from(cfg: HttpServerConfig) -> Self { 24 | Self { 25 | analytics: cfg.analytics, 26 | } 27 | } 28 | } 29 | 30 | /// Enum representing unique identifiers for each page in the application. 31 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 32 | #[serde(rename_all = "snake_case")] 33 | pub(crate) enum PageId { 34 | About, 35 | EmployerDashboard, 36 | JobBoard, 37 | JobSeekerDashboard, 38 | LogIn, 39 | ModeratorDashboard, 40 | NotFound, 41 | SignUp, 42 | Stats, 43 | } 44 | -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/job_contractor.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Contractor* 6 | • _Location:_ *Not provided* 7 | • _Salary:_ *Not provided* 8 | 9 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/job_full_details.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Full time* 6 | • _Location:_ *Berlin, Germany* (hybrid) 7 | • _Seniority:_ *Lead* 8 | • _Salary:_ *EUR 90K - 130K / year* 9 | • _Time working on open source:_ *100%* 10 | • _Time working on upstream projects:_ *75%* 11 | • _Required skills:_ *` Golang `* *` Cloud native `* *` Devops `* *` Leadership `* 12 | 13 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/job_internship.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Internship* 6 | • _Location:_ *Not provided* 7 | • _Salary:_ *Not provided* 8 | 9 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/job_part_time.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Part time* 6 | • _Location:_ *Not provided* 7 | • _Salary:_ *Not provided* 8 | 9 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/job_remote_no_location.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Full time* 6 | • _Location:_ *Remote* 7 | • _Salary:_ *Not provided* 8 | 9 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/job_with_fixed_salary.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Full time* 6 | • _Location:_ *Not provided* 7 | • _Salary:_ *USD 150K / year* 8 | 9 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/job_with_location_hybrid.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Full time* 6 | • _Location:_ *San Francisco, USA* (hybrid) 7 | • _Salary:_ *Not provided* 8 | 9 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/job_with_location_onsite.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Full time* 6 | • _Location:_ *San Francisco, USA* 7 | • _Salary:_ *Not provided* 8 | 9 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/job_with_location_remote.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Full time* 6 | • _Location:_ *San Francisco, USA* (remote) 7 | • _Salary:_ *Not provided* 8 | 9 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/job_with_open_source.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Full time* 6 | • _Location:_ *Not provided* 7 | • _Salary:_ *Not provided* 8 | • _Time working on open source:_ *80%* 9 | 10 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/job_with_salary_min_only.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Full time* 6 | • _Location:_ *Not provided* 7 | • _Salary:_ *GBP 100K / year* 8 | 9 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/job_with_salary_range.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Full time* 6 | • _Location:_ *Not provided* 7 | • _Salary:_ *EUR 120K - 180K / year* 8 | 9 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/job_with_seniority.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Full time* 6 | • _Location:_ *Not provided* 7 | • _Seniority:_ *Senior* 8 | • _Salary:_ *Not provided* 9 | 10 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/job_with_skills.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Full time* 6 | • _Location:_ *Not provided* 7 | • _Salary:_ *Not provided* 8 | • _Required skills:_ *` Rust `* *` Kubernetes `* *` Docker `* 9 | 10 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/job_with_upstream_commitment.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Full time* 6 | • _Location:_ *Not provided* 7 | • _Salary:_ *Not provided* 8 | • _Time working on upstream projects:_ *50%* 9 | 10 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/templates/testdata/minimal_job.golden: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | *Software Engineer* at *ACME Corp* 4 | 5 | • _Job type:_ *Full time* 6 | • _Location:_ *Not provided* 7 | • _Salary:_ *Not provided* 8 | 9 | For more details please see: https://example.com/?job_id=550e8400-e29b-41d4-a716-446655440000 -------------------------------------------------------------------------------- /gitjobs-server/src/workers.rs: -------------------------------------------------------------------------------- 1 | //! This module contains background workers for some tasks. 2 | 3 | use std::time::Duration; 4 | 5 | use tokio::time::sleep; 6 | use tokio_util::{sync::CancellationToken, task::TaskTracker}; 7 | use tracing::{debug, error}; 8 | 9 | use crate::db::DynDB; 10 | 11 | /// Launches all background workers. 12 | pub(crate) fn run(db: DynDB, tracker: &TaskTracker, cancellation_token: CancellationToken) { 13 | // Jobs archiver 14 | tracker.spawn(async move { 15 | archiver(db, cancellation_token).await; 16 | }); 17 | } 18 | 19 | /// Worker that archives expired jobs periodically. 20 | pub(crate) async fn archiver(db: DynDB, cancellation_token: CancellationToken) { 21 | // Random sleep to avoid multiple workers running at the same time 22 | tokio::select! { 23 | () = sleep(Duration::from_secs(rand::random_range(60..300))) => {}, 24 | () = cancellation_token.cancelled() => return, 25 | } 26 | 27 | loop { 28 | // Archive expired jobs 29 | debug!("archiving expired jobs"); 30 | if let Err(err) = db.archive_expired_jobs().await { 31 | error!("error archiving expired jobs: {err}"); 32 | } 33 | 34 | // Pause for a while before the next iteration 35 | tokio::select! { 36 | () = sleep(Duration::from_secs(60*60)) => {}, 37 | () = cancellation_token.cancelled() => break, 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cncf/gitjobs/7835dc18e436d262635d1b11f3caebcbc9685657/gitjobs-server/static/images/background.jpg -------------------------------------------------------------------------------- /gitjobs-server/static/images/badge_member.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cncf/gitjobs/7835dc18e436d262635d1b11f3caebcbc9685657/gitjobs-server/static/images/badge_member.png -------------------------------------------------------------------------------- /gitjobs-server/static/images/gitjobs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cncf/gitjobs/7835dc18e436d262635d1b11f3caebcbc9685657/gitjobs-server/static/images/gitjobs.png -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/applications.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/archive.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/arrow_left.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/arrow_left_double.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/arrow_right.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/arrow_right_double.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/bluesky.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/briefcase.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/buildings.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/cancel.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/caret_down.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/caret_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/company.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/draft.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/email.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/eraser.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/external_link.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/file_badge.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/gear.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/github.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/graduation_cap.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/hour_glass.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/image.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/lfx.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/list.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/live.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/location.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/login.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/logout.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/medal.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/microphone.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/money.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/office.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/office_chair.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/organigram.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/outline_clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/pending_invitation.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/phone.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/plus_bottom.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/plus_top.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/project.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/remote.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/save.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/send.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/signature.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/stats.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/tasks.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/team.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/user.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/user_plus.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/vertical_dots.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/icons/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/index/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cncf/gitjobs/7835dc18e436d262635d1b11f3caebcbc9685657/gitjobs-server/static/images/index/apple-touch-icon.png -------------------------------------------------------------------------------- /gitjobs-server/static/images/index/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cncf/gitjobs/7835dc18e436d262635d1b11f3caebcbc9685657/gitjobs-server/static/images/index/favicon.ico -------------------------------------------------------------------------------- /gitjobs-server/static/images/index/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/index/gitjobs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cncf/gitjobs/7835dc18e436d262635d1b11f3caebcbc9685657/gitjobs-server/static/images/index/gitjobs.png -------------------------------------------------------------------------------- /gitjobs-server/static/images/spinner/spinner_1.svg: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/spinner/spinner_2.svg: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/spinner/spinner_3.svg: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/spinner/spinner_4.svg: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /gitjobs-server/static/images/spinner/spinner_5.svg: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /gitjobs-server/static/js/.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | printWidth: 110 2 | trailingComma: "all" 3 | -------------------------------------------------------------------------------- /gitjobs-server/static/js/common/data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a list of available job benefits. 3 | * Used for multiselect and filter components. 4 | * @returns {string[]} Array of benefit identifiers 5 | */ 6 | export const getBenefits = () => { 7 | return [ 8 | "401k", 9 | "flexible-hours", 10 | "remote-friendly", 11 | "health-insurance", 12 | "paid-time-off", 13 | "4-day-workweek", 14 | "company-retreats", 15 | "home-office-budget", 16 | "learning-budget", 17 | "mental-wellness-bugdet", 18 | "equity-compensation", 19 | "no-whiteboard-interview", 20 | ]; 21 | }; 22 | 23 | /** 24 | * Returns a list of available technical skills. 25 | * Used for multiselect and filter components. 26 | * @returns {string[]} Array of skill identifiers 27 | */ 28 | export const getSkills = () => { 29 | return [ 30 | "kubernetes", 31 | "docker", 32 | "aws", 33 | "gcp", 34 | "azure", 35 | "terraform", 36 | "linux", 37 | "helm", 38 | "prometheus", 39 | "python", 40 | "golang", 41 | "rust", 42 | "jenkins", 43 | "java", 44 | "git", 45 | "devops", 46 | "ansible", 47 | "ci/cd", 48 | "sre", 49 | "security", 50 | "containers", 51 | "oci", 52 | "c++", 53 | "serverless", 54 | "automation", 55 | "microservices", 56 | "service-mesh", 57 | ]; 58 | }; 59 | -------------------------------------------------------------------------------- /gitjobs-server/static/js/common/header.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Toggles the user dropdown menu visibility and manages event listeners. 3 | * Handles click-outside-to-close functionality. 4 | */ 5 | export const onClickDropdown = () => { 6 | const dropdownButton = document.getElementById("user-dropdown-button"); 7 | const dropdownMenu = document.getElementById("dropdown-user"); 8 | 9 | if (dropdownMenu) { 10 | const isHidden = dropdownMenu.classList.contains("hidden"); 11 | 12 | if (isHidden) { 13 | dropdownMenu.classList.remove("hidden"); 14 | 15 | const menuLinks = dropdownMenu.querySelectorAll("a"); 16 | menuLinks.forEach((link) => { 17 | // Close dropdown actions when clicking on an action before loading the new page 18 | link.addEventListener("htmx:beforeOnLoad", () => { 19 | const menu = document.getElementById("dropdown-user"); 20 | menu.classList.add("hidden"); 21 | }); 22 | }); 23 | 24 | if (dropdownButton) { 25 | // Close dropdown actions when clicking outside 26 | document.addEventListener("click", (event) => { 27 | if (!dropdownMenu.contains(event.target) && !dropdownButton.contains(event.target)) { 28 | dropdownMenu.classList.add("hidden"); 29 | } 30 | }); 31 | } 32 | } else { 33 | dropdownMenu.classList.add("hidden"); 34 | // TODO: Store and remove the actual event listener function 35 | } 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /gitjobs-server/static/js/common/lit-wrapper.js: -------------------------------------------------------------------------------- 1 | import { LitElement } from "/static/vendor/js/lit-all.v3.2.1.min.js"; 2 | 3 | /** 4 | * Base wrapper class for Lit components that disables shadow DOM. 5 | * Allows components to use global Tailwind CSS styles. 6 | * @extends LitElement 7 | */ 8 | export class LitWrapper extends LitElement { 9 | /** 10 | * Creates the render root for the component. 11 | * Disables shadow DOM to enable global CSS access. 12 | * Clears innerHTML when re-rendering to prevent duplicate content. 13 | * @returns {LitWrapper} The component instance as render root 14 | */ 15 | createRenderRoot() { 16 | if (this.children.length === 0) { 17 | // Disable shadow DOM to use Tailwind CSS 18 | return this; 19 | } else { 20 | // Remove previous content when re-rendering full component 21 | this.innerHTML = ""; 22 | // Disable shadow DOM to use Tailwind CSS 23 | return this; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /gitjobs-server/static/js/common/markdown-editor.js: -------------------------------------------------------------------------------- 1 | import { html, createRef, ref } from "/static/vendor/js/lit-all.v3.2.1.min.js"; 2 | import { LitWrapper } from "/static/js/common/lit-wrapper.js"; 3 | 4 | /** 5 | * Markdown editor component using EasyMDE. 6 | * Provides rich text editing with markdown support. 7 | * @extends LitWrapper 8 | */ 9 | export class MarkdownEditor extends LitWrapper { 10 | /** 11 | * Component properties definition 12 | * @property {string} id - Editor ID attribute 13 | * @property {string} name - Form input name 14 | * @property {string} content - Initial markdown content 15 | * @property {boolean} required - Whether input is required 16 | * @property {Function} onChange - Callback for content changes 17 | * @property {boolean} mini - Use compact editor layout 18 | */ 19 | static properties = { 20 | id: { type: String }, 21 | name: { type: String }, 22 | content: { type: String }, 23 | required: { type: Boolean }, 24 | onChange: { type: Function }, 25 | mini: { type: Boolean }, 26 | }; 27 | 28 | /** @type {import('lit').Ref} Reference to textarea */ 29 | textareaRef = createRef(); 30 | 31 | constructor() { 32 | super(); 33 | this.id = "id"; 34 | this.name = undefined; 35 | this.content = ""; 36 | this.required = false; 37 | this.onChange = undefined; 38 | this.mini = false; 39 | } 40 | 41 | firstUpdated() { 42 | super.firstUpdated(); 43 | 44 | const textarea = this.textareaRef.value; 45 | if (!textarea) { 46 | return; 47 | } 48 | 49 | this._initEditor(textarea); 50 | } 51 | 52 | /** 53 | * Initializes the EasyMDE editor instance. 54 | * @param {HTMLTextAreaElement} textarea - The textarea element to enhance 55 | * @private 56 | */ 57 | _initEditor(textarea) { 58 | const markdownEditor = new EasyMDE({ 59 | element: textarea, 60 | forceSync: true, 61 | hideIcons: ["side-by-side", "fullscreen", "guide", "image", "code"], 62 | showIcons: ["code", "table", "undo", "redo", "horizontal-rule"], 63 | initialValue: this.content, 64 | status: false, 65 | previewClass: "markdown", 66 | // Fix for hidden textarea 67 | autoRefresh: { delay: 300 }, 68 | }); 69 | 70 | markdownEditor.codemirror.on("change", () => { 71 | if (this.onChange) { 72 | this.onChange(markdownEditor.value()); 73 | } 74 | }); 75 | 76 | // Show textarea to avoid console errors with required attribute 77 | textarea.style.display = "block"; 78 | } 79 | 80 | render() { 81 | return html` 82 |
83 | 89 |
90 | `; 91 | } 92 | } 93 | customElements.define("markdown-editor", MarkdownEditor); 94 | -------------------------------------------------------------------------------- /gitjobs-server/static/js/dashboard/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Opens the mobile navigation drawer menu. 3 | * Adds transition effects and manages backdrop visibility. 4 | */ 5 | export const openNavigationDrawer = () => { 6 | const navigationDrawer = document.getElementById("drawer-menu"); 7 | if (navigationDrawer) { 8 | navigationDrawer.classList.add("transition-transform"); 9 | navigationDrawer.classList.remove("-translate-x-full"); 10 | navigationDrawer.dataset.open = "true"; 11 | } 12 | const backdrop = document.getElementById("drawer-backdrop"); 13 | if (backdrop) { 14 | backdrop.classList.remove("hidden"); 15 | } 16 | }; 17 | 18 | /** 19 | * Closes the mobile navigation drawer menu. 20 | * Removes transition effects and resets scroll position. 21 | */ 22 | export const closeNavigationDrawer = () => { 23 | const navigationDrawer = document.getElementById("drawer-menu"); 24 | if (navigationDrawer) { 25 | navigationDrawer.classList.add("-translate-x-full"); 26 | navigationDrawer.classList.remove("transition-transform"); 27 | navigationDrawer.dataset.open = "false"; 28 | navigationDrawer.scrollTop = 0; 29 | } 30 | const backdrop = document.getElementById("drawer-backdrop"); 31 | if (backdrop) { 32 | backdrop.classList.add("hidden"); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /gitjobs-server/static/js/dashboard/employer/jobs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Triggers an HTMX action on a form element. 3 | * @param {string} formId - The ID of the form element 4 | * @param {string} action - The action to trigger 5 | */ 6 | export const triggerActionOnForm = (formId, action) => { 7 | const form = document.getElementById(formId); 8 | if (form) { 9 | htmx.trigger(form, action); 10 | } 11 | }; 12 | 13 | /** 14 | * Validates and adjusts salary fields based on selected salary type. 15 | * Ensures proper required attributes for range vs exact salary. 16 | */ 17 | export const checkSalaryBeforeSubmit = () => { 18 | const salaryPeriodField = document.querySelector('select[name="salary_period"]'); 19 | const salaryCurrencyField = document.querySelector('select[name="salary_currency"]'); 20 | const salaryField = document.querySelector('input[name="salary"]'); 21 | const salaryMinField = document.querySelector('input[name="salary_min"]'); 22 | const salaryMaxField = document.querySelector('input[name="salary_max"]'); 23 | const selectedSalaryType = document.querySelector('input[name="salary_kind"]:checked'); 24 | 25 | // Ensure all fields are present before proceeding 26 | if ( 27 | !salaryPeriodField || 28 | !salaryCurrencyField || 29 | !salaryField || 30 | !salaryMinField || 31 | !salaryMaxField || 32 | !selectedSalaryType 33 | ) { 34 | return; 35 | } 36 | 37 | // Clear all required attributes initially 38 | salaryPeriodField.removeAttribute("required"); 39 | salaryCurrencyField.removeAttribute("required"); 40 | salaryField.removeAttribute("required"); 41 | salaryMinField.removeAttribute("required"); 42 | salaryMaxField.removeAttribute("required"); 43 | 44 | if (selectedSalaryType.id === "range") { 45 | // Range salary: clear exact value, set requirements for range fields 46 | salaryField.value = ""; 47 | 48 | if (salaryMinField.value !== "" || salaryMaxField.value !== "") { 49 | salaryMinField.setAttribute("required", "required"); 50 | salaryMaxField.setAttribute("required", "required"); 51 | salaryPeriodField.setAttribute("required", "required"); 52 | salaryCurrencyField.setAttribute("required", "required"); 53 | } 54 | } else { 55 | // Exact salary: clear range values, set requirements for exact fields 56 | salaryMinField.value = ""; 57 | salaryMaxField.value = ""; 58 | 59 | if (salaryField.value !== "") { 60 | salaryField.setAttribute("required", "required"); 61 | salaryPeriodField.setAttribute("required", "required"); 62 | salaryCurrencyField.setAttribute("required", "required"); 63 | } 64 | } 65 | }; 66 | 67 | /** 68 | * Validates job title to prevent "remote" in title. 69 | * @param {HTMLInputElement} input - The job title input element 70 | */ 71 | export const checkJobTitle = (input) => { 72 | input.setCustomValidity(""); 73 | const jobTitle = input.value.trim(); 74 | if (jobTitle.toLowerCase().includes("remote")) { 75 | input.setCustomValidity("Please use the workplace field to indicate that a job is remote"); 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /gitjobs-server/static/js/dashboard/jobseeker/form.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Displays the specified section and updates navigation state. 3 | * @param {string} section - The section identifier to display 4 | */ 5 | export const displayActiveSection = (section) => { 6 | const navigationButton = document.querySelector(`[data-section=${section}]`); 7 | const isActive = navigationButton.getAttribute("data-active"); 8 | if (isActive === "false" && navigationButton) { 9 | const allButtons = document.querySelectorAll("[data-section]"); 10 | allButtons.forEach((button) => { 11 | button.setAttribute("data-active", "false"); 12 | button.classList.remove("active"); 13 | }); 14 | navigationButton.setAttribute("data-active", "true"); 15 | navigationButton.classList.add("active"); 16 | 17 | const allSections = document.querySelectorAll("[data-content]"); 18 | allSections.forEach((content) => { 19 | if (content.getAttribute("data-content") !== section) { 20 | content.classList.add("hidden"); 21 | } else { 22 | content.classList.remove("hidden"); 23 | } 24 | }); 25 | } 26 | }; 27 | 28 | /** 29 | * Validates all job seeker profile forms. 30 | * Shows first invalid section if validation fails. 31 | * @returns {boolean} True if all forms are valid 32 | */ 33 | export const validateFormData = () => { 34 | const formSections = ["profile", "experience", "education", "projects"]; 35 | 36 | for (const formName of formSections) { 37 | const formElement = document.getElementById(`${formName}-form`); 38 | 39 | if (!formElement.checkValidity()) { 40 | displayActiveSection(formName); 41 | formElement.reportValidity(); 42 | return false; 43 | } 44 | } 45 | 46 | return true; 47 | }; 48 | -------------------------------------------------------------------------------- /gitjobs-server/static/vendor/fonts/inter-latin-ext.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cncf/gitjobs/7835dc18e436d262635d1b11f3caebcbc9685657/gitjobs-server/static/vendor/fonts/inter-latin-ext.woff2 -------------------------------------------------------------------------------- /gitjobs-server/static/vendor/fonts/inter-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cncf/gitjobs/7835dc18e436d262635d1b11f3caebcbc9685657/gitjobs-server/static/vendor/fonts/inter-latin.woff2 -------------------------------------------------------------------------------- /gitjobs-server/templates/.djlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "custom_blocks": "when", 3 | "format_attribute_template_tags": true, 4 | "format_js": true, 5 | "ignore": "J004,J018", 6 | "ignore_blocks": "call,when", 7 | "indent": 2, 8 | "js": { 9 | "indent_size": 2 10 | }, 11 | "max_blank_lines": 1, 12 | "max_line_length": "110" 13 | } 14 | -------------------------------------------------------------------------------- /gitjobs-server/templates/common_base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" -%} 2 | 3 | {% block content -%} 4 | {# Lit components -#} 5 | 6 | 7 | 8 | 9 | {# End lit components -#} 10 | 11 | {# Header -#} 12 | 15 | {# End Header -#} 16 | 17 |
18 | {% block main -%} 19 | {% endblock main -%} 20 |
21 | {% endblock content -%} 22 | -------------------------------------------------------------------------------- /gitjobs-server/templates/common_header.html: -------------------------------------------------------------------------------- 1 | {% extends "header.html" -%} 2 | {% import "macros.html" as macros -%} 3 | 4 | {% block user_menu -%} 5 | {% call macros::user_menu(user.name, user.username) -%} 6 | {% endblock user_menu -%} 7 | -------------------------------------------------------------------------------- /gitjobs-server/templates/dashboard/dashboard_macros.html: -------------------------------------------------------------------------------- 1 | {# Menu title -#} 2 | {% macro menu_title(text, extra_styles = "") -%} 3 |
{{ text }}
4 | {% endmacro menu_title -%} 5 | {# End menu title -#} 6 | 7 | {# Menu item -#} 8 | {% macro menu_item(name, icon, is_active, href, disabled = false, extra_styles = "", icon_size = "size-4", items_count = 0 ) -%} 9 | {% if is_active -%} 10 | 28 | {% else -%} 29 | {% if disabled -%} 30 | 38 | {% else -%} 39 | 56 | {% endif -%} 57 | {% endif -%} 58 | {% endmacro menu_item -%} 59 | {# End menu item -#} 60 | -------------------------------------------------------------------------------- /gitjobs-server/templates/dashboard/employer/employers/initial_setup.html: -------------------------------------------------------------------------------- 1 | {% import "macros.html" as macros -%} 2 | 3 |
4 |
Set up your first employer
5 | 6 |

7 | Before being able to post jobs, you need to set up an employer. We will ask you for some details, like the company's name, location or description. This information will be used in your jobs postings, but you can update it anytime. 8 |

9 | 10 |

11 | You can set up multiple employers from your account if you'd like. The employer selector on the navigation menu on the left allows you to switch between them. 12 |

13 | 14 |
15 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /gitjobs-server/templates/dashboard/employer/jobs/preview.html: -------------------------------------------------------------------------------- 1 | {% import "misc/job_preview.html" as job_preview_macros -%} 2 | 3 | {% if let Some(logo_id) = employer.logo_id -%} 4 | {% let logo = &self::build_dashboard_image_url(logo_id, "small") -%} 5 | {% call job_preview_macros::job_preview(job = job, employer = employer, logo = logo, employer_description = employer.description) -%} 6 | {% else -%} 7 | {% call job_preview_macros::job_preview(job = job, employer = employer, employer_description = employer.description) -%} 8 | {% endif -%} 9 | -------------------------------------------------------------------------------- /gitjobs-server/templates/dashboard/job_seeker/home.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard/dashboard_base.html" -%} 2 | {% import "macros.html" as macros -%} 3 | {% import "dashboard/dashboard_macros.html" as dashboard_macros -%} 4 | 5 | {% block menu -%} 6 |
7 |
Dashboard
8 |
9 | {% call macros::spinner(size = "size-5") -%} 10 |
11 |
12 | 13 |
14 |
15 | {% call dashboard_macros::menu_title(text = "Job seeker", extra_styles = "py-1.5") %} 16 | {% call dashboard_macros::menu_item(name = "Profile", icon = "briefcase", is_active = content.is_profile(), href = "/dashboard/job-seeker?tab=profile") -%} 17 | {% call dashboard_macros::menu_item(name = "My applications", icon = "applications", is_active = content.is_applications(), href = "/dashboard/job-seeker?tab=applications") -%} 18 |
19 | 20 |
21 | {% call dashboard_macros::menu_title(text = "Account", extra_styles = "py-1.5") -%} 22 | {% call dashboard_macros::menu_item(name = "Account", icon = "user", is_active = content.is_account(), href = "/dashboard/job-seeker?tab=account", icon_size = "size-3.5") -%} 23 |
24 |
25 | {% endblock menu -%} 26 | 27 | {% block dashboard_main -%} 28 |
29 | {# Content -#} 30 | {{ content|safe }} 31 | {# End Content -#} 32 |
33 | {# Messages -#} 34 | {% if !messages.is_empty() -%} 35 | {% call macros::alerts(messages) -%} 36 | {% endif -%} 37 | {# End messages -#} 38 | {% endblock dashboard_main -%} 39 | -------------------------------------------------------------------------------- /gitjobs-server/templates/dashboard/moderator/home.html: -------------------------------------------------------------------------------- 1 | {% extends "dashboard/dashboard_base_moderator.html" -%} 2 | {% import "macros.html" as macros -%} 3 | {% import "dashboard/dashboard_macros.html" as dashboard_macros -%} 4 | 5 | {% block menu -%} 6 |
7 |
Dashboard
8 |
9 | {% call macros::spinner(size = "size-5") -%} 10 |
11 |
12 | 13 |
14 |
15 | {% call dashboard_macros::menu_title(text = "Jobs", extra_styles = "py-1.5") %} 16 | {% call dashboard_macros::menu_item(name = "Pending", icon = "tasks", is_active = content.is_pending_jobs(), href = "/dashboard/moderator?tab=pending-jobs") -%} 17 | {% call dashboard_macros::menu_item(name = "Live", icon = "live", is_active = content.is_live_jobs(), href = "/dashboard/moderator?tab=live-jobs") -%} 18 |
19 |
20 | {% endblock menu -%} 21 | 22 | {% block dashboard_main -%} 23 |
27 | {# Content -#} 28 | {{ content|safe }} 29 | {# End Content -#} 30 |
31 | {# Messages -#} 32 | {% if !messages.is_empty() -%} 33 | {% call macros::alerts(messages) -%} 34 | {% endif -%} 35 | {# End messages -#} 36 | {% endblock dashboard_main -%} 37 | -------------------------------------------------------------------------------- /gitjobs-server/templates/dashboard/moderator/live_jobs.html: -------------------------------------------------------------------------------- 1 | {% import "macros.html" as macros -%} 2 | {% import "dashboard/moderator/moderator_macros.html" as moderator_macros -%} 3 | 4 |
5 | {# Mobile filters button -#} 6 |
7 | 11 | 21 |
22 | {# End mobile filters button -#} 23 | {% call macros::form_title(title = "Live jobs") -%} 24 |
25 | 26 | {# Live jobs Table -#} 27 | {% call moderator_macros::jobs_table(jobs = jobs) -%} 28 | {# End live jobs Table -#} 29 | 30 | {# Mobile live jobs cards -#} 31 |
32 | {% if jobs.is_empty() -%} 33 | 40 | {% else -%} 41 | {% for job in jobs -%} 42 | {% call moderator_macros::mobile_job_card(job = job) -%} 43 | {% endfor -%} 44 | {% endif -%} 45 |
46 | {# End mobile live jobs cards -#} 47 | 48 | {# Preview modal -#} 49 | {% call moderator_macros::preview_modal() -%} 50 | {# End preview modal -#} 51 | 52 | {# Reject modal -#} 53 | {% call moderator_macros::reject_modal() -%} 54 | {# End reject modal -#} 55 | -------------------------------------------------------------------------------- /gitjobs-server/templates/dashboard/moderator/pending_jobs.html: -------------------------------------------------------------------------------- 1 | {% import "macros.html" as macros -%} 2 | {% import "dashboard/moderator/moderator_macros.html" as moderator_macros -%} 3 | 4 |
5 | {# Mobile filters button -#} 6 |
7 | 11 | 21 |
22 | {# End mobile filters button -#} 23 | {% call macros::form_title(title = "Moderation pending jobs") -%} 24 |
25 | 26 | {# Pending jobs Table -#} 27 | {% call moderator_macros::jobs_table(jobs = jobs, kind = "pending") -%} 28 | {# End pending jobs Table -#} 29 | 30 | {# Mobile pending jobs cards -#} 31 |
32 | {% if jobs.is_empty() -%} 33 | 40 | {% else -%} 41 | {% for job in jobs -%} 42 | {% call moderator_macros::mobile_job_card(job = job, kind = "pending") -%} 43 | {% endfor -%} 44 | {% endif -%} 45 |
46 | {# End mobile pending jobs cards -#} 47 | 48 | {# Preview modal -#} 49 | {% call moderator_macros::preview_modal() -%} 50 | {# End preview modal -#} 51 | 52 | {# Reject modal -#} 53 | {% call moderator_macros::reject_modal() -%} 54 | {# End reject modal -#} 55 | -------------------------------------------------------------------------------- /gitjobs-server/templates/footer.html: -------------------------------------------------------------------------------- 1 | {# Footer -#} 2 |
3 |
4 | {# Copyright section -#} 5 |
Copyright © 2025 The Linux Foundation®. All rights reserved.
6 | 7 |
8 | The Linux Foundation has registered trademarks and uses trademarks. For a list of trademarks of The Linux Foundation, please see our Trademark Usage page. Linux is a registered trademark of Linus Torvalds. Privacy Policy and Terms of Use. 18 |
19 | {# End copyright section -#} 20 | 21 |
22 | Powered by CNCF GitJobs. 26 |
27 |
28 |
29 | {# End footer -#} 30 | -------------------------------------------------------------------------------- /gitjobs-server/templates/jobboard/about/page.html: -------------------------------------------------------------------------------- 1 | {% extends "common_base.html" %} 2 | 3 | {% block main -%} 4 |
5 |
6 |

7 | Discover Open Source job opportunities 8 |

9 |
10 |
11 |
12 | {{ content|safe }} 13 |
14 |
15 |
16 | {% endblock main -%} 17 | -------------------------------------------------------------------------------- /gitjobs-server/templates/jobboard/embed/jobs_page.html: -------------------------------------------------------------------------------- 1 | {% import "jobboard/jobs/results_section.html" as macros -%} 2 | 3 | {# djlint:off H030 H031 #} 4 | 5 | 6 | GitJobs 7 | 8 | 9 | 10 |
11 | {# GitJobs logo -#} 12 | 31 | {# End GitJobs logo -#} 32 | 33 |
34 | {# When jobs is empty -#} 35 | {% if jobs.len() == 0 -%} 36 | 42 | {% else -%} 43 | {% for job in jobs -%} 44 | {% let open_source = job.open_source.unwrap_or_default() -%} 45 | {% let upstream_commitment = job.upstream_commitment.unwrap_or_default() -%} 46 | 47 | {% call macros::job_card(job = job) -%} 51 | {% endfor -%} 52 | {% endif -%} 53 |
54 |
55 | 57 | 58 | 59 | {# djlint:on #} 60 | -------------------------------------------------------------------------------- /gitjobs-server/templates/jobboard/jobs/jobs.html: -------------------------------------------------------------------------------- 1 | {% extends "common_base.html" -%} 2 | 3 | {% block main -%} 4 |
5 |
6 |

7 | Discover Open Source job opportunities 8 |

9 |
10 | {{ explore_section|safe }} 11 |
12 | 13 | {# Preview modal -#} 14 |
15 | {# End preview modal -#} 16 | {% endblock main -%} 17 | -------------------------------------------------------------------------------- /gitjobs-server/templates/misc/not_found.html: -------------------------------------------------------------------------------- 1 | {% extends "common_base.html" -%} 2 | 3 | {% block main -%} 4 |
5 |
6 |
7 |
8 |
9 |
Error 404 - Page Not Found
10 |

The page you were looking for wasn't found.

11 | 12 | {# Button home -#} 13 |
14 | 15 | Go to Home 16 |
17 | {# End button home -#} 18 |
19 |
20 | {% endblock main -%} 21 | -------------------------------------------------------------------------------- /gitjobs-server/templates/misc/preview_modal.html: -------------------------------------------------------------------------------- 1 | {% macro modal(on_close = &"") -%} 2 | {# Preview modal -#} 3 | 24 | {# End preview modal -#} 25 | 26 | {# djlint:off #} 27 | 51 | {# djlint:on #} 52 | {% endmacro modal -%} 53 | -------------------------------------------------------------------------------- /gitjobs-server/templates/misc/user_menu_section.html: -------------------------------------------------------------------------------- 1 | {% import "header.html" as macros_header -%} 2 | 3 | {% call macros_header::user_menu(user, auth_provider) -%} 4 | -------------------------------------------------------------------------------- /gitjobs-server/templates/notifications/email_macros.html: -------------------------------------------------------------------------------- 1 | {% macro email_button(link, text = "") -%} 2 | {# djlint:off H021 #} 3 | 11 | 12 | 13 | 46 | 47 | 48 |
17 | 24 | 25 | 26 | 42 | 43 | 44 |
31 | {{ text }} 41 |
45 |
49 | 57 | 58 | 59 | 70 | 71 | 72 |
64 |

67 | Or you can copy-paste this link: {{ link }} 68 |

69 |
73 | {# djlint:on H021 #} 74 | {% endmacro email_button %} 75 | -------------------------------------------------------------------------------- /gitjobs-server/templates/notifications/email_verification.html: -------------------------------------------------------------------------------- 1 | {% extends "notifications/base.html" -%} 2 | {% import "notifications/email_macros.html" as macros %} 3 | 4 | {% block subject -%} 5 | Verify your email address 6 | {% endblock subject -%} 7 | 8 | {% block preheader -%} 9 | Welcome to GitJobs! 10 | {% endblock preheader -%} 11 | 12 | {% block content -%} 13 |

14 | Welcome to GitJobs!! 15 |
16 |
17 | Please note that the verification code is only valid for 24 hours. If you haven't verified your account by then you'll need to sign up again. 18 |

19 | 20 | {% call macros::email_button(link = link, text = "Verify your email" ) %} 21 | 22 |

23 | Once you've verified your email, you'll be able to log in using your credentials. 24 |
25 |
26 | Thanks for joining us! 27 |

28 | {% endblock content -%} 29 | 30 | {% block footer -%} 31 | Didn't create an account? I's likely someone just typed in your email address by accident. 32 |
33 | Feel free to ignore this email. 34 | {% endblock footer -%} 35 | -------------------------------------------------------------------------------- /gitjobs-server/templates/notifications/slack_job_published.md: -------------------------------------------------------------------------------- 1 | New job published! 2 | 3 | {# Job title and company -#} 4 | *{{ job.title }}* at *{{ job.employer.company }}* 5 | {#- End job title and company #} 6 | 7 | {# Job type -#} 8 | • _Job type:_ *{{ &job.kind.to_string()|unnormalize|capitalize }}* 9 | {# End job type -#} 10 | {# Location -#} 11 | • _Location:_{{ " " }} 12 | {%- if let Some(location) = job.location -%} 13 | *{{ location.city }}, {{ location.country }}*{{ " " }} 14 | {%- if job.workplace == Workplace::Remote -%} 15 | (remote) 16 | {%- else if job.workplace == Workplace::Hybrid -%} 17 | (hybrid) 18 | {%- endif -%} 19 | {%- else -%} 20 | {%- if job.workplace == Workplace::Remote -%} 21 | *Remote* 22 | {%- else -%} 23 | *Not provided* 24 | {%- endif -%} 25 | {%- endif %} 26 | {# End location -#} 27 | {# Seniority -#} 28 | {% if let Some(seniority) = job.seniority -%} 29 | • _Seniority:_ *{{ &seniority.to_string()|unnormalize|capitalize }}* 30 | {% endif -%} 31 | {# End seniority -#} 32 | {# Salary -#} 33 | • _Salary:_{{ " " }}* 34 | {%- if let Some(salary) = job.salary -%} 35 | {%- if let Some(salary_currency) = job.salary_currency -%} 36 | {{ salary_currency }}{{ " " }} 37 | {%- endif -%} 38 | {{ salary|humanize_salary }}{{ " " }} 39 | {%- if let Some(salary_period) = job.salary_period -%} 40 | / {{ salary_period }} 41 | {%- endif -%}* 42 | {%- else if let Some(salary_min) = job.salary_min -%} 43 | {%- if let Some(salary_currency) = job.salary_currency -%} 44 | {{ salary_currency }}{{ " " }} 45 | {%- endif -%} 46 | {{ salary_min|humanize_salary }}{{ " " }} 47 | {%- if let Some(salary_max) = job.salary_max -%} 48 | - {{ salary_max|humanize_salary }}{{ " " }} 49 | {%- endif -%} 50 | {%- if let Some(salary_period) = job.salary_period -%} 51 | / {{ salary_period }} 52 | {%- endif -%}* 53 | {%- else -%} 54 | Not provided* 55 | {%- endif %} 56 | {# End salary -#} 57 | {# Open source -#} 58 | {% if let Some(open_source) = job.open_source -%} 59 | • _Time working on open source:_ *{{ open_source }}%* 60 | {% endif -%} 61 | {# End open source -#} 62 | {# Upstream commitment -#} 63 | {% if let Some(upstream_commitment) = job.upstream_commitment -%} 64 | • _Time working on upstream projects:_ *{{ upstream_commitment }}%* 65 | {% endif -%} 66 | {# End upstream commitment -#} 67 | {# Skills -#} 68 | {% if let Some(skills) = job.skills -%} 69 | • _Required skills:_ {% for skill in skills.iter().take(5) -%}*` {{ skill|unnormalize|capitalize }} `* {% endfor %} 70 | {% endif -%} 71 | {# End skills -#} 72 | {{ " " }} 73 | For more details please see: {{ base_url }}/?job_id={{ job.job_id }} 74 | -------------------------------------------------------------------------------- /gitjobs-server/templates/notifications/team_invitation.html: -------------------------------------------------------------------------------- 1 | {% extends "notifications/base.html" -%} 2 | {% import "notifications/email_macros.html" as macros %} 3 | 4 | {% block subject -%} 5 | Team invitation from GitJobs 6 | {% endblock subject -%} 7 | 8 | {% block preheader -%} 9 | Team invitation from GitJobs 10 | {% endblock preheader -%} 11 | 12 | {% block content -%} 13 |

14 | You've been invited to join an employer's team at GitJobs. You can accept or reject this invitation from the employer dashboard 15 |

16 | 17 | {% call macros::email_button(link = link, text = "Employer dashboard" ) %} 18 | {% endblock content -%} 19 | -------------------------------------------------------------------------------- /gitjobs-syncer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gitjobs-syncer" 3 | description = "GitJobs foundations members and projects synchronization tool" 4 | version.workspace = true 5 | license.workspace = true 6 | edition.workspace = true 7 | rust-version.workspace = true 8 | 9 | [dependencies] 10 | anyhow = { workspace = true } 11 | async-trait = { workspace = true } 12 | clap = { workspace = true } 13 | deadpool-postgres = { workspace = true } 14 | figment = { workspace = true } 15 | futures = { workspace = true } 16 | openssl = { workspace = true } 17 | postgres-openssl = { workspace = true } 18 | regex = { workspace = true } 19 | reqwest = { workspace = true } 20 | serde = { workspace = true } 21 | tokio = { workspace = true } 22 | tokio-postgres = { workspace = true } 23 | tracing = { workspace = true } 24 | tracing-subscriber = { workspace = true } 25 | 26 | [dev-dependencies] 27 | -------------------------------------------------------------------------------- /gitjobs-syncer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build syncer 2 | FROM rust:1-alpine3.21 AS builder 3 | RUN apk --no-cache add musl-dev perl make 4 | WORKDIR /gitjobs 5 | COPY Cargo.* ./ 6 | COPY gitjobs-server/Cargo.* gitjobs-server 7 | COPY gitjobs-syncer gitjobs-syncer 8 | WORKDIR /gitjobs/gitjobs-syncer 9 | RUN cargo build --release 10 | 11 | # Final stage 12 | FROM alpine:3.21.3 13 | RUN apk --no-cache add ca-certificates && addgroup -S gitjobs && adduser -S gitjobs -G gitjobs 14 | USER gitjobs 15 | WORKDIR /home/gitjobs 16 | COPY --from=builder /gitjobs/target/release/gitjobs-syncer /usr/local/bin 17 | -------------------------------------------------------------------------------- /gitjobs-syncer/src/config.rs: -------------------------------------------------------------------------------- 1 | //! This module defines types for the syncer configuration. 2 | 3 | use std::path::PathBuf; 4 | 5 | use anyhow::Result; 6 | use deadpool_postgres::Config as DbConfig; 7 | use figment::{ 8 | Figment, 9 | providers::{Env, Format, Serialized, Yaml}, 10 | }; 11 | use serde::{Deserialize, Serialize}; 12 | use tracing::instrument; 13 | 14 | /// Server configuration. 15 | #[derive(Debug, Clone, Deserialize, Serialize)] 16 | pub(crate) struct Config { 17 | /// Database configuration. 18 | pub db: DbConfig, 19 | /// Logging configuration. 20 | pub log: LogConfig, 21 | } 22 | 23 | impl Config { 24 | /// Create a new Config instance. 25 | #[instrument(err)] 26 | pub(crate) fn new(config_file: Option<&PathBuf>) -> Result { 27 | let mut figment = Figment::new().merge(Serialized::default("log.format", "json")); 28 | 29 | if let Some(config_file) = config_file { 30 | figment = figment.merge(Yaml::file(config_file)); 31 | } 32 | 33 | figment 34 | .merge(Env::prefixed("GITJOBS_").split("__")) 35 | .extract() 36 | .map_err(Into::into) 37 | } 38 | } 39 | 40 | /// Logs configuration. 41 | /// 42 | /// Specifies the format for application logs. 43 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 44 | pub(crate) struct LogConfig { 45 | /// Format to use for logs. 46 | pub format: LogFormat, 47 | } 48 | 49 | /// Format to use in logs. 50 | /// 51 | /// Supported formats for log output. 52 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 53 | #[serde(rename_all = "snake_case")] 54 | pub(crate) enum LogFormat { 55 | /// JSON log format. 56 | Json, 57 | /// Human-readable pretty log format. 58 | Pretty, 59 | } 60 | -------------------------------------------------------------------------------- /gitjobs-syncer/src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, clippy::pedantic)] 2 | #![allow(clippy::struct_field_names)] 3 | 4 | use std::{path::PathBuf, sync::Arc}; 5 | 6 | use anyhow::{Context, Result}; 7 | use clap::Parser; 8 | use config::{Config, LogFormat}; 9 | use db::PgDB; 10 | use deadpool_postgres::Runtime; 11 | use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; 12 | use postgres_openssl::MakeTlsConnector; 13 | use syncer::Syncer; 14 | use tracing_subscriber::EnvFilter; 15 | 16 | mod config; 17 | mod db; 18 | mod syncer; 19 | 20 | /// Command-line arguments for the application. 21 | #[derive(Debug, Parser)] 22 | #[clap(author, version, about)] 23 | struct Args { 24 | /// Optional path to the configuration file. 25 | #[clap(short, long)] 26 | config_file: Option, 27 | } 28 | 29 | /// Main entry point for the application. 30 | #[tokio::main] 31 | async fn main() -> Result<()> { 32 | // Setup configuration 33 | let args = Args::parse(); 34 | let cfg = Config::new(args.config_file.as_ref()).context("error setting up configuration")?; 35 | 36 | // Setup logging 37 | let ts = tracing_subscriber::fmt() 38 | .with_env_filter( 39 | EnvFilter::try_from_default_env() 40 | .unwrap_or_else(|_| format!("{}=debug", env!("CARGO_CRATE_NAME")).into()), 41 | ) 42 | .with_file(true) 43 | .with_line_number(true); 44 | match cfg.log.format { 45 | LogFormat::Json => ts.json().init(), 46 | LogFormat::Pretty => ts.init(), 47 | } 48 | 49 | // Setup database 50 | let mut builder = SslConnector::builder(SslMethod::tls())?; 51 | builder.set_verify(SslVerifyMode::NONE); 52 | let connector = MakeTlsConnector::new(builder.build()); 53 | let pool = cfg.db.create_pool(Some(Runtime::Tokio1), connector)?; 54 | let db = Arc::new(PgDB::new(pool)); 55 | 56 | // Run syncer 57 | Syncer::new(db).run().await?; 58 | 59 | Ok(()) 60 | } 61 | --------------------------------------------------------------------------------