├── .dockerignore ├── .github ├── FUNDING.yml └── workflows │ ├── dev.yml │ ├── nightly.yml │ └── release.yml ├── .gitignore ├── .yarnrc.yml ├── README.md ├── SECURITY.md ├── apps ├── api │ ├── .env.example │ ├── .gitignore │ ├── Dockerfile │ ├── LICENSE │ ├── README.md │ ├── docker-compose.yml │ ├── package.json │ ├── src │ │ ├── controllers │ │ │ ├── auth.ts │ │ │ ├── clients.ts │ │ │ ├── config.ts │ │ │ ├── data.ts │ │ │ ├── monitoring.ts │ │ │ ├── notebook.ts │ │ │ ├── notifications.ts │ │ │ ├── queue.ts │ │ │ ├── roles.ts │ │ │ ├── storage.ts │ │ │ ├── ticket.ts │ │ │ ├── time.ts │ │ │ ├── users.ts │ │ │ └── webhooks.ts │ │ ├── lib │ │ │ ├── auth.ts │ │ │ ├── checks.ts │ │ │ ├── hog.ts │ │ │ ├── imap.ts │ │ │ ├── jwt.ts │ │ │ ├── nodemailer │ │ │ │ ├── auth │ │ │ │ │ └── forgot-password.ts │ │ │ │ ├── ticket │ │ │ │ │ ├── assigned.ts │ │ │ │ │ ├── comment.ts │ │ │ │ │ ├── create.ts │ │ │ │ │ └── status.ts │ │ │ │ └── transport.ts │ │ │ ├── notifications │ │ │ │ ├── issue │ │ │ │ │ ├── assigned.ts │ │ │ │ │ ├── comment.ts │ │ │ │ │ ├── priority.ts │ │ │ │ │ └── status.ts │ │ │ │ └── webhook.ts │ │ │ ├── roles.ts │ │ │ ├── services │ │ │ │ ├── auth.service.ts │ │ │ │ └── imap.service.ts │ │ │ ├── session.ts │ │ │ ├── types │ │ │ │ ├── email.ts │ │ │ │ └── permissions.ts │ │ │ └── utils │ │ │ │ ├── oauth_client.ts │ │ │ │ ├── oidc_client.ts │ │ │ │ └── saml_client.ts │ │ ├── main.ts │ │ ├── prisma.ts │ │ ├── prisma │ │ │ ├── migrations │ │ │ │ ├── 20230219190916_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230219231320_null_allow │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230220001329_email_queue │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230220002242_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230220005811_fromimap │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230221233223_uuid │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230227225201_autoinc │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230607224601_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230608222751_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230608225933_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230608230406_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230609005323_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230609201306_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230610133106_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230610155640_ticket_status │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230613194311_timetracking │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20230613195745_updatetime │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231123183949_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231124180831_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231125042344_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231125221631_hidden │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231126212553_onboarding │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231130184144_uptime_kb_config │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231130184716_sso │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231130185305_allow │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231201011858_redirect │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231202030625_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231202030940_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231202031821_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231203164241_ │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231203224035_code │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231206001238_interanl_notifications │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20231206002327_remove_title │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240322235917_templates │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240323000015_emailtemplate_recock │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240325230914_external_user │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240330132748_storage │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240330224718_notifications │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20240531225221_createdby │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241014201131_openid │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241014203742_authentication │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241014210350_cleanup │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241018192053_drop_col │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241020165331_oauth_email_authentication │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241021215357_accesstokensmtp │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241021220058_bigint │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241021230158_redirecturi │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241102020326_replyto_support │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241104215009_imap_oauth │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241106185045_imap_password_optional │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241106233134_lock │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241106234810_cascade │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241111235707_edited_comment │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241113193825_rbac │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241113195413_role_active │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241113200612_update_types │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241114164206_sessions │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241115235800_following │ │ │ │ │ └── migration.sql │ │ │ │ ├── 20241116014522_hold │ │ │ │ │ └── migration.sql │ │ │ │ └── migration_lock.toml │ │ │ ├── schema.prisma │ │ │ └── seed.js │ │ └── routes.ts │ └── tsconfig.json ├── client │ ├── .dockerignore │ ├── .env.example │ ├── .gitignore │ ├── @ │ │ └── shadcn │ │ │ ├── block │ │ │ └── GlobalShortcut.tsx │ │ │ ├── components │ │ │ ├── app-sidebar.tsx │ │ │ ├── command-menu.tsx │ │ │ ├── forbidden.tsx │ │ │ ├── nav-main.tsx │ │ │ ├── nav-projects.tsx │ │ │ ├── nav-user.tsx │ │ │ ├── team-switcher.tsx │ │ │ └── tickets │ │ │ │ ├── DisplaySettings.tsx │ │ │ │ ├── FilterBadge.tsx │ │ │ │ ├── TicketFilters.tsx │ │ │ │ ├── TicketKanban.tsx │ │ │ │ ├── TicketList.tsx │ │ │ │ └── ViewSettings.tsx │ │ │ ├── hooks │ │ │ ├── use-media-query.tsx │ │ │ ├── use-mobile.tsx │ │ │ ├── use-toast.ts │ │ │ ├── useTicketActions.ts │ │ │ ├── useTicketFilters.ts │ │ │ └── useTicketView.ts │ │ │ ├── lib │ │ │ ├── hasAccess.ts │ │ │ ├── types │ │ │ │ ├── email.ts │ │ │ │ └── permissions.ts │ │ │ └── utils.ts │ │ │ ├── types │ │ │ └── tickets.ts │ │ │ └── ui │ │ │ ├── alert-dialog.tsx │ │ │ ├── avatar.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── command.tsx │ │ │ ├── context-menu.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── popover.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── switch.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ └── tooltip.tsx │ ├── LICENSE │ ├── components.json │ ├── components │ │ ├── AccountDropdown │ │ │ └── index.tsx │ │ ├── BlockEditor │ │ │ └── index.tsx │ │ ├── ClientNotesModal │ │ │ └── index.js │ │ ├── Combo │ │ │ └── index.tsx │ │ ├── CreateEmailQueue │ │ │ └── index.js │ │ ├── CreateTeamModal │ │ │ └── index.js │ │ ├── CreateTicketModal │ │ │ └── index.tsx │ │ ├── LinkTicketModal │ │ │ └── index.js │ │ ├── ListUserFiles │ │ │ └── index.js │ │ ├── NotebookEditor │ │ │ └── index.tsx │ │ ├── NotificationsSettingsModal │ │ │ └── index.js │ │ ├── ResetPassword │ │ │ └── index.tsx │ │ ├── ThemeSettings │ │ │ └── index.tsx │ │ ├── TicketDetails │ │ │ └── index.tsx │ │ ├── TicketFiles │ │ │ └── index.js │ │ ├── TicketViews │ │ │ └── admin.tsx │ │ ├── TicketsMobileList │ │ │ └── index.js │ │ ├── TransferTicket │ │ │ └── index.js │ │ ├── UpdateClientModal │ │ │ └── index.js │ │ ├── UpdateUserModal │ │ │ └── index.tsx │ │ ├── UserProfile │ │ │ └── index.js │ │ └── Webhooks │ │ │ └── index.js │ ├── i18n.js │ ├── layouts │ │ ├── adminLayout.tsx │ │ ├── newLayout.tsx │ │ ├── notebook.tsx │ │ ├── portalLayout.tsx │ │ ├── settings.tsx │ │ └── shad.tsx │ ├── lib │ │ └── cookie │ │ │ └── index.js │ ├── locales │ │ ├── da │ │ │ └── peppermint.json │ │ ├── de │ │ │ └── peppermint.json │ │ ├── en │ │ │ └── peppermint.json │ │ ├── es │ │ │ └── peppermint.json │ │ ├── fr │ │ │ └── peppermint.json │ │ ├── he │ │ │ └── peppermint.json │ │ ├── hu │ │ │ └── peppermint.json │ │ ├── is │ │ │ └── peppermint.json │ │ ├── it │ │ │ └── peppermint.json │ │ ├── no │ │ │ └── peppermint.json │ │ ├── pt │ │ │ └── peppermint.json │ │ ├── se │ │ │ └── peppermint.json │ │ ├── th │ │ │ └── peppermint.json │ │ ├── tl │ │ │ └── peppermint.json │ │ ├── tr │ │ │ └── peppermint.json │ │ └── zh-CN │ │ │ └── peppermint.json │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── 404.tsx │ │ ├── _app.tsx │ │ ├── _document.js │ │ ├── admin │ │ │ ├── authentication.tsx │ │ │ ├── clients │ │ │ │ ├── index.tsx │ │ │ │ └── new.tsx │ │ │ ├── email-queues │ │ │ │ ├── index.tsx │ │ │ │ ├── new.tsx │ │ │ │ └── oauth.tsx │ │ │ ├── index.js │ │ │ ├── logs.tsx │ │ │ ├── roles │ │ │ │ ├── [id].tsx │ │ │ │ ├── index.tsx │ │ │ │ └── new.tsx │ │ │ ├── smtp │ │ │ │ ├── index.tsx │ │ │ │ ├── oauth.tsx │ │ │ │ └── templates │ │ │ │ │ └── [id].tsx │ │ │ ├── tickets.tsx │ │ │ ├── users │ │ │ │ └── internal │ │ │ │ │ ├── index.js │ │ │ │ │ └── new.tsx │ │ │ └── webhooks.tsx │ │ ├── auth │ │ │ ├── forgot-password.tsx │ │ │ ├── login.tsx │ │ │ ├── oauth.tsx │ │ │ ├── oidc.tsx │ │ │ ├── register.tsx │ │ │ └── reset-password.tsx │ │ ├── documents │ │ │ ├── [id].tsx │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── issue │ │ │ └── [id].tsx │ │ ├── issues │ │ │ ├── closed.tsx │ │ │ ├── index.tsx │ │ │ └── open.tsx │ │ ├── new.tsx │ │ ├── notifications.tsx │ │ ├── onboarding.tsx │ │ ├── portal │ │ │ ├── index.tsx │ │ │ ├── issue │ │ │ │ └── [id].tsx │ │ │ ├── issues │ │ │ │ ├── closed.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── open.tsx │ │ │ └── new.tsx │ │ ├── profile.tsx │ │ ├── queue │ │ │ └── [id].js │ │ ├── settings │ │ │ ├── flags.tsx │ │ │ ├── index.tsx │ │ │ ├── notifications.tsx │ │ │ ├── password.tsx │ │ │ └── sessions.tsx │ │ └── submit.tsx │ ├── postcss.config.js │ ├── public │ │ ├── 404.svg │ │ ├── discord.svg │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ └── site.webmanifest │ │ ├── github.svg │ │ ├── login.svg │ │ ├── logo.svg │ │ └── manifest.json │ ├── store │ │ └── session.js │ ├── styles │ │ └── globals.css │ ├── tailwind.config.js │ └── tsconfig.json ├── docs │ ├── next.config.mjs │ ├── package.json │ ├── pages │ │ ├── _app.jsx │ │ ├── _meta.js │ │ ├── development.md │ │ ├── docker.md │ │ ├── index.md │ │ ├── installer.md │ │ ├── oidc.md │ │ ├── proxy.md │ │ └── translation.md │ ├── public │ │ └── favicon.ico │ ├── seo.config.js │ └── theme.config.jsx └── landing │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ ├── dashboard.jpeg │ ├── favicon.ico │ └── thirteen.svg │ ├── src │ ├── app │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ └── component │ │ └── Fathom.tsx │ ├── tailwind.config.ts │ └── tsconfig.json ├── docker-compose.dev.yml ├── docker-compose.local.yml ├── docker-compose.yml ├── dockerfile ├── ecosystem.config.js ├── license ├── package.json ├── packages ├── config │ ├── eslint-preset.js │ └── package.json └── tsconfig │ ├── base.json │ ├── nextjs.json │ ├── node16.json │ └── package.json ├── static ├── black-logo.svg ├── black-side-logo.svg ├── create_a_ticket.png ├── detail.png ├── homepage.png ├── logo.svg └── tickets.png ├── tsconfig.json ├── turbo.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | # Exclude files and directories generated by the build process 2 | node_modules/ 3 | dist/ 4 | 5 | # Exclude development and debugging files 6 | .DS_Store 7 | .vscode/ 8 | 9 | # # Exclude any sensitive or secret files 10 | # .env 11 | # .env.* 12 | 13 | # Exclude any temporary or cache files 14 | *.log 15 | *.tmp 16 | *.swp 17 | 18 | lib/ 19 | .github 20 | .yarn 21 | 22 | apps/api/node_modules 23 | apps/api/dist 24 | apps/client/node_modules -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ['Peppermint-Lab'] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: Dev Client Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - next 7 | 8 | jobs: 9 | build: 10 | runs-on: self-hosted 11 | steps: 12 | - name: Get current time 13 | uses: 1466587594/get-current-time@v2 14 | id: current-time 15 | with: 16 | format: YYYY-MM-DD--HH 17 | 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v1 23 | 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v1 26 | 27 | - name: Login to DockerHub 28 | uses: docker/login-action@v1 29 | with: 30 | username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 32 | 33 | - name: Build and push 34 | uses: docker/build-push-action@v2 35 | with: 36 | platforms: linux/amd64,linux/arm64 37 | push: true 38 | tags: | 39 | pepperlabs/peppermint:dev 40 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Get current time 13 | uses: 1466587594/get-current-time@v2 14 | id: current-time 15 | with: 16 | format: YYYY-MM-DD--HH 17 | 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | # Install QEMU-based emulator 22 | - name: Install QEMU 23 | run: apt-get update && apt-get install -y qemu-user-static 24 | 25 | - name: Set up QEMU 26 | uses: docker/setup-qemu-action@v1 27 | 28 | - name: Set up Docker Buildx 29 | uses: docker/setup-buildx-action@v2 30 | 31 | - name: Login to DockerHub 32 | uses: docker/login-action@v1 33 | with: 34 | username: ${{ secrets.DOCKERHUB_USERNAME }} 35 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 36 | 37 | - name: Build and push 38 | uses: docker/build-push-action@v2 39 | with: 40 | platforms: linux/amd64,linux/arm64 41 | push: true 42 | tags: | 43 | pepperlabs/peppermint:nightly 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Build 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: self-hosted 9 | steps: 10 | - name: Get current time 11 | uses: 1466587594/get-current-time@v2 12 | id: current-time 13 | with: 14 | format: YYYY-MM-DD--HH 15 | 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | # Install QEMU-based emulator 20 | - name: Install QEMU 21 | run: apt-get update && apt-get install -y qemu-user-static 22 | 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@v1 25 | 26 | - name: Set up Docker Buildx 27 | uses: docker/setup-buildx-action@v2 28 | 29 | - name: Login to DockerHub 30 | uses: docker/login-action@v1 31 | with: 32 | username: ${{ secrets.DOCKERHUB_USERNAME }} 33 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 34 | 35 | - name: Build and push 36 | uses: docker/build-push-action@v2 37 | with: 38 | platforms: linux/amd64,linux/arm64 39 | push: true 40 | tags: | 41 | pepperlabs/peppermint:latest 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | .yarn 8 | .pnp.cjs 9 | .pnp.loader.mjs 10 | 11 | # testing 12 | coverage 13 | 14 | # next.js 15 | .next/ 16 | out/ 17 | build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # turbo 36 | .turbo 37 | 38 | # vercel 39 | .vercel 40 | 41 | # Typescript build 42 | dist 43 | 44 | infra 45 | 46 | .cursorrules -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | 23 | If you find any issues please contact me on twitter @potts_dev 24 | -------------------------------------------------------------------------------- /apps/api/.env.example: -------------------------------------------------------------------------------- 1 | DB_USERNAME="peppermint" 2 | DB_PASSWORD="1234" 3 | DB_HOST=localhost 4 | DATABASE_URL="postgresql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/peppermint" 5 | SECRET="supersecret" 6 | 7 | -------------------------------------------------------------------------------- /apps/api/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | /uploads 107 | 108 | logs.log -------------------------------------------------------------------------------- /apps/api/Dockerfile: -------------------------------------------------------------------------------- 1 | # FROM node:16 2 | 3 | # ENV port=8090 4 | # COPY . . 5 | 6 | # RUN yarn 7 | # RUN yarn build 8 | 9 | # EXPOSE 8090 10 | # CMD [ "node", "./dist/main.js" ] 11 | 12 | FROM node:18 AS base 13 | 14 | # The web Dockerfile is copy-pasted into our main docs at /docs/handbook/deploying-with-docker. 15 | # Make sure you update this Dockerfile, the Dockerfile in the web workspace and copy that over to Dockerfile in the docs. 16 | 17 | FROM base AS builder 18 | 19 | # Set working directory 20 | WORKDIR /app 21 | RUN yarn global add turbo 22 | COPY . . 23 | RUN turbo prune --scope=api --docker 24 | 25 | 26 | # Add lockfile and package.json's of isolated subworkspace 27 | FROM base AS installer 28 | WORKDIR /app 29 | 30 | # First install dependencies (as they change less often) 31 | COPY .gitignore .gitignore 32 | COPY --from=builder /app/out/json/ . 33 | COPY --from=builder /app/out/yarn.lock ./yarn.lock 34 | RUN yarn install 35 | 36 | 37 | # Build the project and its dependencies 38 | COPY --from=builder /app/out/full/ . 39 | RUN ls 40 | COPY turbo.json /../../turbo.json 41 | 42 | # RUN rm -f apps/api/.env 43 | 44 | RUN yarn turbo run build --filter=database 45 | 46 | FROM base AS runner 47 | WORKDIR /app 48 | 49 | # Don't run production as root 50 | # RUN addgroup --system --gid 1001 prod 51 | # RUN adduser --system --uid 1001 prod 52 | # USER prod 53 | COPY --from=installer /app . 54 | 55 | EXPOSE 8090 56 | CMD ["node", "apps/api/dist/"] 57 | -------------------------------------------------------------------------------- /apps/api/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Satish Babariya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/api/README.md: -------------------------------------------------------------------------------- 1 | # api 2 | 3 | Clean Architecture for node.js projects using fastify and prisma 4 | 5 | ``` 6 | src 7 | │ main.ts # Application entry point\ 8 | └───controllers # route controllers for all the endpoints of the app 9 | └───middlewares # route middleware 10 | └───prisma # here lies prisma schema and migrations 11 | └───types # Type declaration files (d.ts) for Typescript 12 | 13 | 14 | ``` 15 | -------------------------------------------------------------------------------- /apps/api/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | postgres: 4 | image: "postgres:11" 5 | restart: unless-stopped 6 | environment: 7 | - POSTGRES_USER=pepppermint-dev 8 | - POSTGRES_PASSWORD=12345 9 | - POSTGRES_DB=peppermint 10 | ports: 11 | - '5432:5432' 12 | volumes: 13 | - postgres_data_local_dev:/var/lib/postgresql/data 14 | 15 | mailhog: 16 | image: jcalonso/mailhog 17 | ports: 18 | - 1025:1025 # smtp server 19 | - 8025:8025 # web ui 20 | 21 | volumes: 22 | postgres_data_local_dev: 23 | -------------------------------------------------------------------------------- /apps/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "main": "dist/main.js", 5 | "repository": "", 6 | "author": "Jack Andrews", 7 | "license": "MIT", 8 | "scripts": { 9 | "postinstall": "prisma generate", 10 | "generate": "prisma generate", 11 | "db:push": "prisma db push --accept-data-loss", 12 | "db:migrate": "prisma migrate dev", 13 | "build": "tsc", 14 | "dev": "ts-node-dev --respawn src/main.ts" 15 | }, 16 | "devDependencies": { 17 | "@types/bcrypt": "^5.0.0", 18 | "@types/email-reply-parser": "^1", 19 | "@types/formidable": "^3.4.5", 20 | "@types/imap": "^0.8.42", 21 | "@types/jsonwebtoken": "^8.5.8", 22 | "@types/mailparser": "^3.4.5", 23 | "@types/node": "^17.0.23", 24 | "@types/nodemailer": "^6.4.14", 25 | "@types/passport-local": "^1.0.35", 26 | "@types/simple-oauth2": "^5", 27 | "prisma": "5.6.0", 28 | "ts-node": "^10.7.0", 29 | "ts-node-dev": "^2.0.0", 30 | "typescript": "^5.3.2" 31 | }, 32 | "dependencies": { 33 | "@azure/identity": "^4.5.0", 34 | "@fastify/cookie": "^9.0.4", 35 | "@fastify/cors": "^10.0.1", 36 | "@fastify/multipart": "^8.2.0", 37 | "@fastify/rate-limit": "^9.0.0", 38 | "@fastify/session": "^10.4.0", 39 | "@fastify/swagger": "^9.2.0", 40 | "@fastify/swagger-ui": "^5.1.0", 41 | "@prisma/client": "5.6.0", 42 | "add": "^2.0.6", 43 | "axios": "^1.5.0", 44 | "bcrypt": "^5.0.1", 45 | "dotenv": "^16.0.0", 46 | "email-reply-parser": "^1.8.1", 47 | "fastify": "5.1", 48 | "fastify-formidable": "^3.0.2", 49 | "fastify-multer": "^2.0.3", 50 | "formidable": "^3.5.1", 51 | "google-auth-library": "^9.14.2", 52 | "got": "^13.0.0", 53 | "handlebars": "^4.7.8", 54 | "i": "^0.3.7", 55 | "imap": "^0.8.19", 56 | "jsonwebtoken": "9.0.2", 57 | "lru-cache": "^11.0.1", 58 | "mailparser": "^3.6.5", 59 | "nodemailer": "^6.9.7", 60 | "openid-client": "^5.7.0", 61 | "pino": "^9.5.0", 62 | "posthog-node": "^3.1.3", 63 | "prisma": "5.6.0", 64 | "samlify": "^2.8.11", 65 | "simple-oauth2": "^5.1.0", 66 | "xml-encryption": "^3.0.2" 67 | }, 68 | "prisma": { 69 | "schema": "./src/prisma/schema.prisma", 70 | "seed": "node ./src/prisma/seed.js" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /apps/api/src/controllers/clients.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; 2 | import { track } from "../lib/hog"; 3 | import { requirePermission } from "../lib/roles"; 4 | import { prisma } from "../prisma"; 5 | 6 | export function clientRoutes(fastify: FastifyInstance) { 7 | // Register a new client 8 | fastify.post( 9 | "/api/v1/client/create", 10 | { 11 | preHandler: requirePermission(["client::create"]), 12 | }, 13 | async (request: FastifyRequest, reply: FastifyReply) => { 14 | const { name, email, number, contactName }: any = request.body; 15 | 16 | const client = await prisma.client.create({ 17 | data: { 18 | name, 19 | contactName, 20 | email, 21 | number: String(number), 22 | }, 23 | }); 24 | 25 | const hog = track(); 26 | 27 | hog.capture({ 28 | event: "client_created", 29 | distinctId: client.id, 30 | }); 31 | 32 | reply.send({ 33 | success: true, 34 | }); 35 | } 36 | ); 37 | 38 | // Update client 39 | fastify.post( 40 | "/api/v1/client/update", 41 | { 42 | preHandler: requirePermission(["client::update"]), 43 | }, 44 | async (request: FastifyRequest, reply: FastifyReply) => { 45 | const { name, email, number, contactName, id }: any = request.body; 46 | 47 | await prisma.client.update({ 48 | where: { id: id }, 49 | data: { 50 | name, 51 | contactName, 52 | email, 53 | number: String(number), 54 | }, 55 | }); 56 | 57 | reply.send({ 58 | success: true, 59 | }); 60 | } 61 | ); 62 | 63 | // Get all clients 64 | fastify.get( 65 | "/api/v1/clients/all", 66 | { 67 | preHandler: requirePermission(["client::read"]), 68 | }, 69 | async (request: FastifyRequest, reply: FastifyReply) => { 70 | const clients = await prisma.client.findMany({}); 71 | 72 | reply.send({ 73 | success: true, 74 | clients: clients, 75 | }); 76 | } 77 | ); 78 | 79 | // Delete client 80 | fastify.delete( 81 | "/api/v1/clients/:id/delete-client", 82 | { 83 | preHandler: requirePermission(["client::delete"]), 84 | }, 85 | async (request: FastifyRequest, reply: FastifyReply) => { 86 | const { id }: any = request.params; 87 | 88 | await prisma.client.delete({ 89 | where: { id: id }, 90 | }); 91 | 92 | reply.send({ 93 | success: true, 94 | }); 95 | } 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /apps/api/src/controllers/data.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; 2 | import { requirePermission } from "../lib/roles"; 3 | import { prisma } from "../prisma"; 4 | 5 | export function dataRoutes(fastify: FastifyInstance) { 6 | // Get total count of all tickets 7 | fastify.get( 8 | "/api/v1/data/tickets/all", 9 | { 10 | preHandler: requirePermission(["issue::read"]), 11 | }, 12 | async (request: FastifyRequest, reply: FastifyReply) => { 13 | const result = await prisma.ticket.count({ 14 | where: { hidden: false }, 15 | }); 16 | 17 | reply.send({ count: result }); 18 | } 19 | ); 20 | 21 | // Get total count of all completed tickets 22 | fastify.get( 23 | "/api/v1/data/tickets/completed", 24 | { 25 | preHandler: requirePermission(["issue::read"]), 26 | }, 27 | async (request: FastifyRequest, reply: FastifyReply) => { 28 | const result = await prisma.ticket.count({ 29 | where: { isComplete: true, hidden: false }, 30 | }); 31 | 32 | reply.send({ count: result }); 33 | } 34 | ); 35 | 36 | // Get total count of all open tickets 37 | fastify.get( 38 | "/api/v1/data/tickets/open", 39 | { 40 | preHandler: requirePermission(["issue::read"]), 41 | }, 42 | async (request: FastifyRequest, reply: FastifyReply) => { 43 | const result = await prisma.ticket.count({ 44 | where: { isComplete: false, hidden: false }, 45 | }); 46 | 47 | reply.send({ count: result }); 48 | } 49 | ); 50 | 51 | // Get total of all unsassigned tickets 52 | fastify.get( 53 | "/api/v1/data/tickets/unassigned", 54 | { 55 | preHandler: requirePermission(["issue::read"]), 56 | }, 57 | async (request: FastifyRequest, reply: FastifyReply) => { 58 | const result = await prisma.ticket.count({ 59 | where: { userId: null, hidden: false, isComplete: false }, 60 | }); 61 | 62 | reply.send({ count: result }); 63 | } 64 | ); 65 | 66 | // Get all logs 67 | fastify.get( 68 | "/api/v1/data/logs", 69 | async (request: FastifyRequest, reply: FastifyReply) => { 70 | const logs = await import("fs/promises").then((fs) => 71 | fs.readFile("logs.log", "utf-8") 72 | ); 73 | reply.send({ logs: logs }); 74 | } 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /apps/api/src/controllers/monitoring.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/apps/api/src/controllers/monitoring.ts -------------------------------------------------------------------------------- /apps/api/src/controllers/notifications.ts: -------------------------------------------------------------------------------- 1 | // Create a new notification 2 | // Get All notifications 3 | // Mark as read 4 | // Delete notification 5 | -------------------------------------------------------------------------------- /apps/api/src/controllers/storage.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; 3 | import multer from "fastify-multer"; 4 | import { prisma } from "../prisma"; 5 | const upload = multer({ dest: "uploads/" }); 6 | 7 | export function objectStoreRoutes(fastify: FastifyInstance) { 8 | // 9 | fastify.post( 10 | "/api/v1/storage/ticket/:id/upload/single", 11 | { preHandler: upload.single("file") }, 12 | 13 | async (request: FastifyRequest, reply: FastifyReply) => { 14 | console.log(request.file); 15 | console.log(request.body); 16 | 17 | const uploadedFile = await prisma.ticketFile.create({ 18 | data: { 19 | ticketId: request.params.id, 20 | filename: request.file.originalname, 21 | path: request.file.path, 22 | mime: request.file.mimetype, 23 | size: request.file.size, 24 | encoding: request.file.encoding, 25 | userId: request.body.user, 26 | }, 27 | }); 28 | 29 | console.log(uploadedFile); 30 | 31 | reply.send({ 32 | success: true, 33 | }); 34 | } 35 | ); 36 | 37 | // Get all ticket attachments 38 | 39 | // Delete an attachment 40 | 41 | // Download an attachment 42 | } 43 | -------------------------------------------------------------------------------- /apps/api/src/controllers/time.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; 2 | import { prisma } from "../prisma"; 3 | 4 | export function timeTrackingRoutes(fastify: FastifyInstance) { 5 | // Create a new entry 6 | fastify.post( 7 | "/api/v1/time/new", 8 | 9 | async (request: FastifyRequest, reply: FastifyReply) => { 10 | const { time, ticket, title, user }: any = request.body; 11 | 12 | console.log(request.body); 13 | 14 | await prisma.timeTracking.create({ 15 | data: { 16 | time: Number(time), 17 | title, 18 | userId: user, 19 | ticketId: ticket, 20 | }, 21 | }); 22 | 23 | reply.send({ 24 | success: true, 25 | }); 26 | } 27 | ); 28 | 29 | // Get all entries 30 | 31 | // Delete an entry 32 | } 33 | -------------------------------------------------------------------------------- /apps/api/src/controllers/webhooks.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; 2 | import { track } from "../lib/hog"; 3 | import { requirePermission } from "../lib/roles"; 4 | import { checkSession } from "../lib/session"; 5 | import { prisma } from "../prisma"; 6 | 7 | export function webhookRoutes(fastify: FastifyInstance) { 8 | // Create a new webhook 9 | fastify.post( 10 | "/api/v1/webhook/create", 11 | { 12 | preHandler: requirePermission(["webhook::create"]), 13 | }, 14 | async (request: FastifyRequest, reply: FastifyReply) => { 15 | const user = await checkSession(request); 16 | const { name, url, type, active, secret }: any = request.body; 17 | await prisma.webhooks.create({ 18 | data: { 19 | name, 20 | url, 21 | type, 22 | active, 23 | secret, 24 | createdBy: user!.id, 25 | }, 26 | }); 27 | 28 | const client = track(); 29 | 30 | client.capture({ 31 | event: "webhook_created", 32 | distinctId: "uuid", 33 | }); 34 | 35 | client.shutdownAsync(); 36 | 37 | reply.status(200).send({ message: "Hook created!", success: true }); 38 | } 39 | ); 40 | 41 | // Get all webhooks 42 | fastify.get( 43 | "/api/v1/webhooks/all", 44 | { 45 | preHandler: requirePermission(["webhook::read"]), 46 | }, 47 | async (request: FastifyRequest, reply: FastifyReply) => { 48 | const webhooks = await prisma.webhooks.findMany({}); 49 | 50 | reply.status(200).send({ webhooks: webhooks, success: true }); 51 | } 52 | ); 53 | 54 | // Delete a webhook 55 | fastify.delete( 56 | "/api/v1/admin/webhook/:id/delete", 57 | { 58 | preHandler: requirePermission(["webhook::delete"]), 59 | }, 60 | async (request: FastifyRequest, reply: FastifyReply) => { 61 | const { id }: any = request.params; 62 | await prisma.webhooks.delete({ 63 | where: { 64 | id: id, 65 | }, 66 | }); 67 | 68 | reply.status(200).send({ success: true }); 69 | } 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /apps/api/src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | // functions for easily finding relevant auth methods 2 | 3 | import { prisma } from "../prisma"; 4 | 5 | export async function getOidcConfig() { 6 | const config = await prisma.openIdConfig.findFirst(); 7 | if (!config) { 8 | throw new Error("Config not found in the database"); 9 | } 10 | return config; 11 | } 12 | 13 | export async function getOAuthProvider() { 14 | const provider = await prisma.oAuthProvider.findFirst(); 15 | if (!provider) { 16 | throw new Error(`OAuth provider ${provider} not found`); 17 | } 18 | return provider; 19 | } 20 | 21 | export async function getSAMLProvider(providerName: any) { 22 | const provider = await prisma.sAMLProvider.findUnique({ 23 | where: { name: providerName }, 24 | }); 25 | if (!provider) { 26 | throw new Error(`SAML provider ${providerName} not found`); 27 | } 28 | return provider; 29 | } 30 | -------------------------------------------------------------------------------- /apps/api/src/lib/checks.ts: -------------------------------------------------------------------------------- 1 | import { FastifyReply, FastifyRequest } from "fastify"; 2 | import { checkToken } from "./jwt"; 3 | 4 | // Check valid token 5 | export const authenticateUser = ( 6 | request: FastifyRequest, 7 | reply: FastifyReply, 8 | done: any 9 | ) => { 10 | const bearer = request.headers.authorization!.split(" ")[1]; 11 | const token = checkToken(bearer); 12 | 13 | if (!token) { 14 | return reply.code(401).send({ error: "Unauthorized" }); 15 | } 16 | 17 | // User is authenticated, continue to the route handler 18 | done(); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/api/src/lib/hog.ts: -------------------------------------------------------------------------------- 1 | import { PostHog } from "posthog-node"; 2 | 3 | export function track() { 4 | return new PostHog( 5 | "phc_2gbpy3JPtDC6hHrQy35yMxMci1NY0fD1sttGTcPjwVf", 6 | 7 | { host: "https://app.posthog.com" } 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/api/src/lib/imap.ts: -------------------------------------------------------------------------------- 1 | import { ImapService } from "./services/imap.service"; 2 | 3 | export const getEmails = async (): Promise => { 4 | try { 5 | await ImapService.fetchEmails(); 6 | console.log('Email fetch completed'); 7 | } catch (error) { 8 | console.error('An error occurred while fetching emails:', error); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /apps/api/src/lib/jwt.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | export function checkToken(token: string) { 4 | const bearer = token; 5 | 6 | var b64string = process.env.SECRET; 7 | var buf = new Buffer(b64string!, "base64"); // Ta-da 8 | 9 | const verified = jwt.verify(bearer, buf); 10 | 11 | return verified; 12 | } 13 | -------------------------------------------------------------------------------- /apps/api/src/lib/nodemailer/ticket/assigned.ts: -------------------------------------------------------------------------------- 1 | import handlebars from "handlebars"; 2 | import { prisma } from "../../../prisma"; 3 | import { createTransportProvider } from "../transport"; 4 | 5 | export async function sendAssignedEmail(email: any) { 6 | try { 7 | 8 | const provider = await prisma.email.findFirst(); 9 | 10 | if (provider) { 11 | const mail = await createTransportProvider(); 12 | 13 | console.log("Sending email to: ", email); 14 | 15 | const testhtml = await prisma.emailTemplate.findFirst({ 16 | where: { 17 | type: "ticket_assigned", 18 | }, 19 | }); 20 | 21 | var template = handlebars.compile(testhtml?.html); 22 | var htmlToSend = template({}); // Pass an empty object as the argument to the template function 23 | 24 | await mail 25 | .sendMail({ 26 | from: provider?.reply, 27 | to: email, 28 | subject: `A new ticket has been assigned to you`, 29 | text: `Hello there, a ticket has been assigned to you`, 30 | html: htmlToSend, 31 | }) 32 | .then((info: any) => { 33 | console.log("Message sent: %s", info.messageId); 34 | }) 35 | .catch((err: any) => console.log(err)); 36 | } 37 | } catch (error) { 38 | console.log(error); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/api/src/lib/nodemailer/ticket/comment.ts: -------------------------------------------------------------------------------- 1 | import handlebars from "handlebars"; 2 | import { prisma } from "../../../prisma"; 3 | import { createTransportProvider } from "../transport"; 4 | 5 | export async function sendComment( 6 | comment: string, 7 | title: string, 8 | id: string, 9 | email: string 10 | ) { 11 | try { 12 | const provider = await prisma.email.findFirst(); 13 | 14 | const transport = await createTransportProvider(); 15 | 16 | const testhtml = await prisma.emailTemplate.findFirst({ 17 | where: { 18 | type: "ticket_comment", 19 | }, 20 | }); 21 | 22 | var template = handlebars.compile(testhtml?.html); 23 | var replacements = { 24 | title: title, 25 | comment: comment, 26 | }; 27 | var htmlToSend = template(replacements); 28 | 29 | console.log("Sending email to: ", email); 30 | await transport 31 | .sendMail({ 32 | from: provider?.reply, 33 | to: email, 34 | subject: `New comment on Issue #${title} ref: #${id}`, 35 | text: `Hello there, Issue #${title}, has had an update with a comment of ${comment}`, 36 | html: htmlToSend, 37 | }) 38 | .then((info: any) => { 39 | console.log("Message sent: %s", info.messageId); 40 | }) 41 | .catch((err: any) => console.log(err)); 42 | } catch (error) { 43 | console.log(error); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /apps/api/src/lib/nodemailer/ticket/create.ts: -------------------------------------------------------------------------------- 1 | import handlebars from "handlebars"; 2 | import { prisma } from "../../../prisma"; 3 | import { createTransportProvider } from "../transport"; 4 | 5 | export async function sendTicketCreate(ticket: any) { 6 | try { 7 | const email = await prisma.email.findFirst(); 8 | 9 | if (email) { 10 | const transport = await createTransportProvider(); 11 | 12 | const testhtml = await prisma.emailTemplate.findFirst({ 13 | where: { 14 | type: "ticket_created", 15 | }, 16 | }); 17 | 18 | var template = handlebars.compile(testhtml?.html); 19 | var replacements = { 20 | id: ticket.id, 21 | }; 22 | var htmlToSend = template(replacements); 23 | 24 | await transport 25 | .sendMail({ 26 | from: email?.reply, 27 | to: ticket.email, 28 | subject: `Issue #${ticket.id} has just been created & logged`, 29 | text: `Hello there, Issue #${ticket.id}, which you reported on ${ticket.createdAt}, has now been created and logged`, 30 | html: htmlToSend, 31 | }) 32 | .then((info: any) => { 33 | console.log("Message sent: %s", info.messageId); 34 | }) 35 | .catch((err: any) => console.log(err)); 36 | } 37 | } catch (error) { 38 | console.log(error); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/api/src/lib/nodemailer/ticket/status.ts: -------------------------------------------------------------------------------- 1 | import handlebars from "handlebars"; 2 | import { prisma } from "../../../prisma"; 3 | import { createTransportProvider } from "../transport"; 4 | 5 | export async function sendTicketStatus(ticket: any) { 6 | const email = await prisma.email.findFirst(); 7 | 8 | if (email) { 9 | const transport = await createTransportProvider(); 10 | 11 | const testhtml = await prisma.emailTemplate.findFirst({ 12 | where: { 13 | type: "ticket_status_changed", 14 | }, 15 | }); 16 | 17 | var template = handlebars.compile(testhtml?.html); 18 | var replacements = { 19 | title: ticket.title, 20 | status: ticket.isComplete ? "COMPLETED" : "OUTSTANDING", 21 | }; 22 | var htmlToSend = template(replacements); 23 | 24 | await transport 25 | .sendMail({ 26 | from: email?.reply, 27 | to: ticket.email, 28 | subject: `Issue #${ticket.Number} status is now ${ 29 | ticket.isComplete ? "COMPLETED" : "OUTSTANDING" 30 | }`, 31 | text: `Hello there, Issue #${ticket.Number}, now has a status of ${ 32 | ticket.isComplete ? "COMPLETED" : "OUTSTANDING" 33 | }`, 34 | html: htmlToSend, 35 | }) 36 | .then((info: any) => { 37 | console.log("Message sent: %s", info.messageId); 38 | }) 39 | .catch((err: any) => console.log(err)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/api/src/lib/nodemailer/transport.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../../prisma"; 2 | 3 | const nodemailer = require("nodemailer"); 4 | const { ConfidentialClientApplication } = require("@azure/identity"); 5 | 6 | export async function createTransportProvider() { 7 | const provider = await prisma.email.findFirst({}); 8 | 9 | if (!provider) { 10 | throw new Error("No email provider configured."); 11 | } 12 | 13 | if (provider?.serviceType === "gmail") { 14 | return nodemailer.createTransport({ 15 | service: "gmail", 16 | host: "smtp.gmail.com", 17 | port: 465, 18 | secure: true, 19 | auth: { 20 | type: "OAuth2", 21 | user: provider?.user, 22 | clientId: provider?.clientId, 23 | clientSecret: provider?.clientSecret, 24 | refreshToken: provider?.refreshToken, 25 | accessToken: provider?.accessToken, 26 | expiresIn: provider?.expiresIn, 27 | }, 28 | }); 29 | } else if (provider?.serviceType === "microsoft") { 30 | // Microsoft 31 | const cca = new ConfidentialClientApplication({ 32 | auth: { 33 | clientId: provider?.clientId, 34 | authority: `https://login.microsoftonline.com/${provider?.tenantId}`, 35 | clientSecret: provider?.clientSecret, 36 | }, 37 | }); 38 | 39 | const result = await cca.acquireTokenByClientCredential({ 40 | scopes: ["https://graph.microsoft.com/.default"], 41 | }); 42 | 43 | return nodemailer.createTransport({ 44 | service: "hotmail", 45 | auth: { 46 | type: "OAuth2", 47 | user: provider?.user, 48 | clientId: provider?.clientId, 49 | clientSecret: provider?.clientSecret, 50 | accessToken: result.accessToken, 51 | }, 52 | }); 53 | } else if (provider?.serviceType === "other") { 54 | // Username/password configuration 55 | return nodemailer.createTransport({ 56 | host: provider.host, 57 | port: provider?.port, 58 | secure: provider?.port === "465" ? true : false, // true for 465, false for other ports 59 | auth: { 60 | user: provider?.user, 61 | pass: provider?.pass, 62 | }, 63 | }); 64 | } else { 65 | throw new Error("No valid authentication method configured."); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /apps/api/src/lib/notifications/issue/assigned.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../../../prisma"; 2 | 3 | /** 4 | * Creates assignment notifications for all ticket followers. 5 | * 6 | * @param {object} ticket - The ticket object 7 | * @param {object} assignee - The user object being assigned 8 | * @param {object} assigner - The user object doing the assigning 9 | * @returns {Promise} 10 | */ 11 | export async function assignedNotification( 12 | assignee: any, 13 | ticket: any, 14 | assigner: any 15 | ) { 16 | try { 17 | const text = `Ticket #${ticket.Number} was assigned to ${assignee.name} by ${assigner.name}`; 18 | 19 | // Get all followers of the ticket, ensuring the creator is not already a follower 20 | const followers = [ 21 | ...(ticket.following || []), 22 | ...(ticket.following?.includes(ticket.createdBy.id) 23 | ? [] 24 | : [ticket.createdBy.id]), 25 | ]; 26 | 27 | // Create notifications for all followers (except the assigner) 28 | await prisma.notifications.createMany({ 29 | data: followers 30 | .filter((userId: string) => userId !== assigner.id) 31 | .map((userId: string) => ({ 32 | text, 33 | userId, 34 | ticketId: ticket.id, 35 | })), 36 | }); 37 | } catch (error) { 38 | console.error("Error creating assignment notifications:", error); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/api/src/lib/notifications/issue/comment.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../../../prisma"; 2 | 3 | /** 4 | * Creates comment notifications for all ticket followers. 5 | * 6 | * @param {object} ticket - The ticket object related to the comment. 7 | * @param {object} commenter - The user object who commented. 8 | * @returns {Promise} 9 | */ 10 | export async function commentNotification(issue: any, commenter: any) { 11 | try { 12 | const text = `New comment on #${issue.Number} by ${commenter.name}`; 13 | 14 | // Get all followers of the ticket, ensuring the creator is not already a follower 15 | const followers = [ 16 | ...(issue.following || []), 17 | ...(issue.following?.includes(issue.createdBy.id) 18 | ? [] 19 | : [issue.createdBy.id]), 20 | ]; 21 | 22 | // Create notifications for all followers (except the commenter) 23 | await prisma.notifications.createMany({ 24 | data: followers 25 | .filter((userId: string) => userId !== commenter.id) 26 | .map((userId: string) => ({ 27 | text, 28 | userId, 29 | ticketId: issue.id, 30 | })), 31 | }); 32 | } catch (error) { 33 | console.error("Error creating comment notifications:", error); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/api/src/lib/notifications/issue/priority.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../../../prisma"; 2 | 3 | export async function priorityNotification( 4 | issue: any, 5 | updatedBy: any, 6 | oldPriority: string, 7 | newPriority: string 8 | ) { 9 | try { 10 | const text = `Priority changed on #${issue.Number} from ${oldPriority} to ${newPriority} by ${updatedBy.name}`; 11 | 12 | // Get all followers of the ticket, ensuring the creator is not already a follower 13 | const followers = [ 14 | ...(issue.following || []), 15 | ...(issue.following?.includes(issue.createdBy.id) 16 | ? [] 17 | : [issue.createdBy.id]), 18 | ]; 19 | 20 | // Create notifications for all followers (except the person who updated) 21 | await prisma.notifications.createMany({ 22 | data: followers 23 | .filter((userId: string) => userId !== updatedBy.id) 24 | .map((userId: string) => ({ 25 | text, 26 | userId, 27 | ticketId: issue.id, 28 | })), 29 | }); 30 | } catch (error) { 31 | console.error("Error creating priority change notifications:", error); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/api/src/lib/notifications/issue/status.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "../../../prisma"; 2 | 3 | export async function activeStatusNotification( 4 | ticket: any, 5 | updater: any, 6 | newStatus: string 7 | ) { 8 | try { 9 | const text = `#${ticket.Number} status changed to ${ 10 | newStatus ? "Closed" : "Open" 11 | } by ${updater.name}`; 12 | 13 | // Get all followers of the ticket, ensuring the creator is not already a follower 14 | const followers = [ 15 | ...(ticket.following || []), 16 | ...(ticket.following?.includes(ticket.createdBy.id) 17 | ? [] 18 | : [ticket.createdBy.id]), 19 | ]; 20 | 21 | // Create notifications for all followers (except the updater) 22 | await prisma.notifications.createMany({ 23 | data: followers 24 | .filter((userId: string) => userId !== updater.id) 25 | .map((userId: string) => ({ 26 | text, 27 | userId, 28 | ticketId: ticket.id, 29 | })), 30 | }); 31 | } catch (error) { 32 | console.error("Error creating status change notifications:", error); 33 | } 34 | } 35 | 36 | export async function statusUpdateNotification( 37 | ticket: any, 38 | updater: any, 39 | newStatus: string 40 | ) { 41 | try { 42 | const text = `#${ticket.Number} status changed to ${newStatus} by ${updater.name}`; 43 | 44 | // Get all followers of the ticket, ensuring the creator is not already a follower 45 | const followers = [ 46 | ...(ticket.following || []), 47 | ...(ticket.following?.includes(ticket.createdBy.id) 48 | ? [] 49 | : [ticket.createdBy.id]), 50 | ]; 51 | 52 | // Create notifications for all followers (except the updater) 53 | await prisma.notifications.createMany({ 54 | data: followers 55 | .filter((userId: string) => userId !== updater.id) 56 | .map((userId: string) => ({ 57 | text, 58 | userId, 59 | ticketId: ticket.id, 60 | })), 61 | }); 62 | } catch (error) { 63 | console.error("Error creating status update notifications:", error); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /apps/api/src/lib/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { GoogleAuth } from "google-auth-library"; 2 | import { prisma } from "../../prisma"; 3 | import { EmailQueue } from "../types/email"; 4 | 5 | export class AuthService { 6 | public static generateXOAuth2Token( 7 | username: string, 8 | accessToken: string 9 | ): string { 10 | const authString = [ 11 | `user=${username}`, 12 | `auth=Bearer ${accessToken}`, 13 | "", 14 | "", 15 | ].join("\x01"); 16 | return Buffer.from(authString).toString("base64"); 17 | } 18 | 19 | static async getValidAccessToken(queue: EmailQueue): Promise { 20 | const { clientId, clientSecret, refreshToken, accessToken, expiresIn } = 21 | queue; 22 | 23 | // Check if token is still valid 24 | const now = Math.floor(Date.now() / 1000); 25 | if (accessToken && expiresIn && now < expiresIn) { 26 | return accessToken; 27 | } 28 | 29 | // Initialize GoogleAuth client 30 | const auth = new GoogleAuth({ 31 | clientOptions: { 32 | clientId: clientId, 33 | clientSecret: clientSecret, 34 | }, 35 | }); 36 | 37 | const oauth2Client = auth.fromJSON({ 38 | client_id: clientId, 39 | client_secret: clientSecret, 40 | refresh_token: refreshToken, 41 | }); 42 | 43 | // Refresh the token if expired 44 | const tokenInfo = await oauth2Client.getAccessToken(); 45 | 46 | const expiryDate = expiresIn! + 3600; 47 | 48 | if (tokenInfo.token) { 49 | await prisma.emailQueue.update({ 50 | where: { id: queue.id }, 51 | data: { 52 | accessToken: tokenInfo.token, 53 | expiresIn: expiryDate, 54 | }, 55 | }); 56 | 57 | return tokenInfo.token; 58 | } else { 59 | throw new Error("Unable to refresh access token."); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apps/api/src/lib/session.ts: -------------------------------------------------------------------------------- 1 | import { FastifyRequest } from "fastify"; 2 | import jwt from "jsonwebtoken"; 3 | import { prisma } from "../prisma"; 4 | 5 | // Checks session token and returns user object 6 | export async function checkSession(request: FastifyRequest) { 7 | try { 8 | const bearer = request.headers.authorization?.split(" ")[1]; 9 | if (!bearer) { 10 | return null; 11 | } 12 | 13 | // Verify JWT token is valid 14 | var b64string = process.env.SECRET; 15 | var secret = Buffer.from(b64string!, "base64"); 16 | 17 | try { 18 | jwt.verify(bearer, secret); 19 | } catch (e) { 20 | // Token is invalid or expired 21 | await prisma.session.delete({ 22 | where: { sessionToken: bearer }, 23 | }); 24 | return null; 25 | } 26 | 27 | // Check if session exists and is not expired 28 | const session = await prisma.session.findUnique({ 29 | where: { sessionToken: bearer }, 30 | include: { user: true }, 31 | }); 32 | 33 | if (!session || session.expires < new Date()) { 34 | // Session expired or doesn't exist 35 | if (session) { 36 | await prisma.session.delete({ 37 | where: { id: session.id }, 38 | }); 39 | } 40 | return null; 41 | } 42 | 43 | // Verify the request is coming from the same client 44 | const currentUserAgent = request.headers["user-agent"]; 45 | const currentIp = request.ip; 46 | 47 | if ( 48 | session.userAgent !== currentUserAgent && 49 | session.ipAddress !== currentIp 50 | ) { 51 | // Potential session hijacking attempt - invalidate the session 52 | await prisma.session.delete({ 53 | where: { id: session.id }, 54 | }); 55 | 56 | return null; 57 | } 58 | 59 | return session.user; 60 | } catch (error) { 61 | return null; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /apps/api/src/lib/types/email.ts: -------------------------------------------------------------------------------- 1 | export interface EmailConfig { 2 | user: string; 3 | host: string; 4 | port: number; 5 | tls: boolean; 6 | tlsOptions: { 7 | rejectUnauthorized: boolean; 8 | servername: string; 9 | }; 10 | xoauth2?: string; 11 | password?: string; 12 | } 13 | 14 | export type EmailQueue = { 15 | serviceType: "gmail" | "other"; 16 | id: string; 17 | username: string; 18 | hostname: string; 19 | password?: string; 20 | clientId?: string; 21 | clientSecret?: string; 22 | refreshToken?: string; 23 | accessToken?: string; 24 | expiresIn?: number; 25 | tls?: boolean; 26 | }; 27 | -------------------------------------------------------------------------------- /apps/api/src/lib/types/permissions.ts: -------------------------------------------------------------------------------- 1 | export type IssuePermission = 2 | | 'issue::create' 3 | | 'issue::read' 4 | | 'issue::write' 5 | | 'issue::update' 6 | | 'issue::delete' 7 | | 'issue::assign' 8 | | 'issue::transfer' 9 | | 'issue::comment'; 10 | 11 | export type UserPermission = 12 | | 'user::create' 13 | | 'user::read' 14 | | 'user::update' 15 | | 'user::delete' 16 | | 'user::manage'; 17 | 18 | export type RolePermission = 19 | | 'role::create' 20 | | 'role::read' 21 | | 'role::update' 22 | | 'role::delete' 23 | | 'role::manage'; 24 | 25 | export type TeamPermission = 26 | | 'team::create' 27 | | 'team::read' 28 | | 'team::update' 29 | | 'team::delete' 30 | | 'team::manage'; 31 | 32 | export type ClientPermission = 33 | | 'client::create' 34 | | 'client::read' 35 | | 'client::update' 36 | | 'client::delete' 37 | | 'client::manage'; 38 | 39 | export type KnowledgeBasePermission = 40 | | 'kb::create' 41 | | 'kb::read' 42 | | 'kb::update' 43 | | 'kb::delete' 44 | | 'kb::manage'; 45 | 46 | export type SystemPermission = 47 | | 'settings::view' 48 | | 'settings::manage' 49 | | 'webhook::manage' 50 | | 'integration::manage' 51 | | 'email_template::manage'; 52 | 53 | export type TimeTrackingPermission = 54 | | 'time_entry::create' 55 | | 'time_entry::read' 56 | | 'time_entry::update' 57 | | 'time_entry::delete'; 58 | 59 | export type ViewPermission = 60 | | 'docs::manage' 61 | | 'admin::panel'; 62 | 63 | export type WebhookPermission = 64 | | 'webhook::create' 65 | | 'webhook::read' 66 | | 'webhook::update' 67 | | 'webhook::delete'; 68 | 69 | export type DocumentPermission = 70 | | 'document::create' 71 | | 'document::read' 72 | | 'document::update' 73 | | 'document::delete' 74 | | 'document::manage'; 75 | 76 | export type Permission = 77 | | IssuePermission 78 | | UserPermission 79 | | RolePermission 80 | | TeamPermission 81 | | ClientPermission 82 | | KnowledgeBasePermission 83 | | SystemPermission 84 | | TimeTrackingPermission 85 | | ViewPermission 86 | | WebhookPermission 87 | | DocumentPermission; 88 | 89 | // Useful type for grouping permissions by category 90 | export const PermissionCategories = { 91 | ISSUE: 'Issue Management', 92 | USER: 'User Management', 93 | ROLE: 'Role Management', 94 | TEAM: 'Team Management', 95 | CLIENT: 'Client Management', 96 | KNOWLEDGE_BASE: 'Knowledge Base', 97 | SYSTEM: 'System Settings', 98 | TIME_TRACKING: 'Time Tracking', 99 | VIEW: 'Views', 100 | WEBHOOK: 'Webhook Management', 101 | DOCUMENT: 'Document Management', 102 | } as const; 103 | 104 | export type PermissionCategory = typeof PermissionCategories[keyof typeof PermissionCategories]; 105 | 106 | // Helper type for permission groups 107 | export interface PermissionGroup { 108 | category: PermissionCategory; 109 | permissions: Permission[]; 110 | } 111 | -------------------------------------------------------------------------------- /apps/api/src/lib/utils/oauth_client.ts: -------------------------------------------------------------------------------- 1 | // utils/oauthClients.js 2 | //@ts-nocheck 3 | 4 | const { AuthorizationCode } = require('simple-oauth2'); 5 | 6 | const oauthClients = {}; 7 | 8 | export function getOAuthClient(providerConfig: any) { 9 | const { name } = providerConfig; 10 | if (!oauthClients[name]) { 11 | oauthClients[name] = new AuthorizationCode({ 12 | client: { 13 | id: providerConfig.clientId, 14 | secret: providerConfig.clientSecret, 15 | }, 16 | auth: { 17 | tokenHost: providerConfig.tokenUrl, 18 | authorizeHost: providerConfig.authorizationUrl, 19 | }, 20 | }); 21 | } 22 | return oauthClients[name]; 23 | } 24 | 25 | -------------------------------------------------------------------------------- /apps/api/src/lib/utils/oidc_client.ts: -------------------------------------------------------------------------------- 1 | // utils/oidcClient.js 2 | 3 | import { Issuer } from "openid-client"; 4 | 5 | let oidcClient: any = null; 6 | 7 | export async function getOidcClient(config: any) { 8 | if (!oidcClient) { 9 | const oidcIssuer = await Issuer.discover(config.issuer); 10 | oidcClient = new oidcIssuer.Client({ 11 | client_id: config.clientId, 12 | redirect_uris: [config.redirectUri], 13 | response_types: ["code"], 14 | token_endpoint_auth_method: "none", 15 | }); 16 | } 17 | return oidcClient; 18 | } 19 | -------------------------------------------------------------------------------- /apps/api/src/lib/utils/saml_client.ts: -------------------------------------------------------------------------------- 1 | // utils/samlProviders.js 2 | 3 | const { ServiceProvider, IdentityProvider } = require('samlify'); 4 | 5 | const samlProviders: any = {}; 6 | 7 | function getSamlProvider(providerConfig: any) { 8 | const { name } = providerConfig; 9 | if (!samlProviders[name]) { 10 | // Configure Service Provider (SP) 11 | const sp = ServiceProvider({ 12 | entityID: providerConfig.issuer, 13 | assertionConsumerService: [{ 14 | Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', 15 | Location: providerConfig.acsUrl, 16 | }], 17 | }); 18 | 19 | // Configure Identity Provider (IdP) 20 | const idp = IdentityProvider({ 21 | entityID: providerConfig.entryPoint, 22 | singleSignOnService: [{ 23 | Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', 24 | Location: providerConfig.ssoLoginUrl, 25 | }], 26 | signingCert: providerConfig.cert, 27 | }); 28 | 29 | samlProviders[name] = { sp, idp }; 30 | } 31 | return samlProviders[name]; 32 | } 33 | 34 | module.exports = getSamlProvider; 35 | -------------------------------------------------------------------------------- /apps/api/src/prisma.ts: -------------------------------------------------------------------------------- 1 | import { Hook, PrismaClient, Role, User } from "@prisma/client"; 2 | export const prisma: PrismaClient = new PrismaClient(); 3 | export { Hook, Role, User }; 4 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20230219231320_null_allow/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "Ticket" DROP CONSTRAINT "Ticket_clientId_fkey"; 3 | 4 | -- AlterTable 5 | ALTER TABLE "Ticket" ALTER COLUMN "clientId" DROP NOT NULL; 6 | 7 | -- AddForeignKey 8 | ALTER TABLE "Ticket" ADD CONSTRAINT "Ticket_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE SET NULL ON UPDATE CASCADE; 9 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20230220001329_email_queue/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Imap_Email" ADD COLUMN "emailQueueId" INTEGER, 3 | ALTER COLUMN "from" DROP NOT NULL, 4 | ALTER COLUMN "subject" DROP NOT NULL, 5 | ALTER COLUMN "body" DROP NOT NULL, 6 | ALTER COLUMN "text" DROP NOT NULL, 7 | ALTER COLUMN "html" DROP NOT NULL; 8 | 9 | -- CreateTable 10 | CREATE TABLE "EmailQueue" ( 11 | "id" SERIAL NOT NULL, 12 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 13 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | "name" TEXT NOT NULL, 15 | "username" TEXT NOT NULL, 16 | "password" TEXT NOT NULL, 17 | "hostname" TEXT NOT NULL, 18 | "tls" BOOLEAN NOT NULL DEFAULT true, 19 | "teams" JSONB, 20 | "teamId" INTEGER, 21 | 22 | CONSTRAINT "EmailQueue_pkey" PRIMARY KEY ("id") 23 | ); 24 | 25 | -- AddForeignKey 26 | ALTER TABLE "Imap_Email" ADD CONSTRAINT "Imap_Email_emailQueueId_fkey" FOREIGN KEY ("emailQueueId") REFERENCES "EmailQueue"("id") ON DELETE SET NULL ON UPDATE CASCADE; 27 | 28 | -- AddForeignKey 29 | ALTER TABLE "EmailQueue" ADD CONSTRAINT "EmailQueue_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE SET NULL ON UPDATE CASCADE; 30 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20230220002242_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `teamId` on the `EmailQueue` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "EmailQueue" DROP CONSTRAINT "EmailQueue_teamId_fkey"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "EmailQueue" DROP COLUMN "teamId"; 12 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20230220005811_fromimap/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `levels` on the `Team` table. All the data in the column will be lost. 5 | - Added the required column `fromImap` to the `Ticket` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Team" DROP COLUMN "levels"; 10 | 11 | -- AlterTable 12 | ALTER TABLE "Ticket" ADD COLUMN "fromImap" BOOLEAN NOT NULL; 13 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20230227225201_autoinc/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Ticket" ADD COLUMN "Number" SERIAL NOT NULL; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20230607224601_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Comment" ( 3 | "id" TEXT NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "text" TEXT NOT NULL, 6 | "public" BOOLEAN NOT NULL DEFAULT false, 7 | "userId" TEXT NOT NULL, 8 | "ticketId" TEXT NOT NULL, 9 | 10 | CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "Comment" ADD CONSTRAINT "Comment_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "Ticket"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20230608222751_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ALTER COLUMN "password" DROP NOT NULL; 3 | 4 | -- CreateTable 5 | CREATE TABLE "Account" ( 6 | "id" TEXT NOT NULL, 7 | "userId" TEXT NOT NULL, 8 | "type" TEXT NOT NULL, 9 | "provider" TEXT NOT NULL, 10 | "providerAccountId" TEXT NOT NULL, 11 | "refresh_token" TEXT, 12 | "access_token" TEXT, 13 | "expires_at" INTEGER, 14 | "token_type" TEXT, 15 | "scope" TEXT, 16 | "id_token" TEXT, 17 | "session_state" TEXT, 18 | 19 | CONSTRAINT "Account_pkey" PRIMARY KEY ("id") 20 | ); 21 | 22 | -- CreateTable 23 | CREATE TABLE "Session" ( 24 | "id" TEXT NOT NULL, 25 | "sessionToken" TEXT NOT NULL, 26 | "userId" TEXT NOT NULL, 27 | "expires" TIMESTAMP(3) NOT NULL, 28 | 29 | CONSTRAINT "Session_pkey" PRIMARY KEY ("id") 30 | ); 31 | 32 | -- CreateTable 33 | CREATE TABLE "VerificationToken" ( 34 | "identifier" TEXT NOT NULL, 35 | "token" TEXT NOT NULL, 36 | "expires" TIMESTAMP(3) NOT NULL 37 | ); 38 | 39 | -- CreateIndex 40 | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); 41 | 42 | -- CreateIndex 43 | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); 44 | 45 | -- CreateIndex 46 | CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); 47 | 48 | -- CreateIndex 49 | CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); 50 | 51 | -- AddForeignKey 52 | ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 53 | 54 | -- AddForeignKey 55 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 56 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20230608225933_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Account" ADD COLUMN "refresh_token_expires_in" INTEGER; 3 | 4 | -- AlterTable 5 | ALTER TABLE "User" ADD COLUMN "emailVerified" BOOLEAN, 6 | ADD COLUMN "image" TEXT; 7 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20230608230406_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ALTER COLUMN "isAdmin" SET DEFAULT false; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20230609005323_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Provider" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "clientId" TEXT NOT NULL, 6 | "clientSecre" TEXT NOT NULL, 7 | "active" BOOLEAN NOT NULL, 8 | "issuer" TEXT, 9 | "tenantId" TEXT, 10 | 11 | CONSTRAINT "Provider_pkey" PRIMARY KEY ("id") 12 | ); 13 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20230609201306_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `clientSecre` on the `Provider` table. All the data in the column will be lost. 5 | - Added the required column `clientSecret` to the `Provider` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Provider" DROP COLUMN "clientSecre", 10 | ADD COLUMN "clientSecret" TEXT NOT NULL; 11 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20230610133106_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Ticket" ALTER COLUMN "name" DROP NOT NULL, 3 | ALTER COLUMN "email" DROP NOT NULL; 4 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20230610155640_ticket_status/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "TicketStatus" AS ENUM ('needs_support', 'in_progress', 'in_review', 'done'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "Ticket" ADD COLUMN "status" "TicketStatus" NOT NULL DEFAULT 'needs_support'; 6 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20230613194311_timetracking/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "TimeTracking" ( 3 | "id" TEXT NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | "title" TEXT NOT NULL, 7 | "comment" TEXT, 8 | "startTime" TIMESTAMP(3) NOT NULL, 9 | "endTime" TIMESTAMP(3) NOT NULL, 10 | "minutesDiff" INTEGER NOT NULL, 11 | "clientId" TEXT, 12 | "userId" TEXT, 13 | "ticketId" TEXT, 14 | 15 | CONSTRAINT "TimeTracking_pkey" PRIMARY KEY ("id") 16 | ); 17 | 18 | -- AddForeignKey 19 | ALTER TABLE "TimeTracking" ADD CONSTRAINT "TimeTracking_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 20 | 21 | -- AddForeignKey 22 | ALTER TABLE "TimeTracking" ADD CONSTRAINT "TimeTracking_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "Client"("id") ON DELETE SET NULL ON UPDATE CASCADE; 23 | 24 | -- AddForeignKey 25 | ALTER TABLE "TimeTracking" ADD CONSTRAINT "TimeTracking_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "Ticket"("id") ON DELETE SET NULL ON UPDATE CASCADE; 26 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20230613195745_updatetime/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `endTime` on the `TimeTracking` table. All the data in the column will be lost. 5 | - You are about to drop the column `minutesDiff` on the `TimeTracking` table. All the data in the column will be lost. 6 | - You are about to drop the column `startTime` on the `TimeTracking` table. All the data in the column will be lost. 7 | - Added the required column `time` to the `TimeTracking` table without a default value. This is not possible if the table is not empty. 8 | 9 | */ 10 | -- AlterTable 11 | ALTER TABLE "TimeTracking" DROP COLUMN "endTime", 12 | DROP COLUMN "minutesDiff", 13 | DROP COLUMN "startTime", 14 | ADD COLUMN "time" INTEGER NOT NULL; 15 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231123183949_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Made the column `password` on table `User` required. This step will fail if there are existing NULL values in that column. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "User" ALTER COLUMN "password" SET NOT NULL; 9 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231124180831_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ALTER COLUMN "password" DROP NOT NULL; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231125042344_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "TicketType" AS ENUM ('bug', 'feature', 'support', 'incident', 'service', 'maintenance', 'access', 'feedback'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "Ticket" ADD COLUMN "type" "TicketType" NOT NULL DEFAULT 'support'; 6 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231125221631_hidden/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Ticket" ADD COLUMN "hidden" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231126212553_onboarding/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "firstLogin" BOOLEAN NOT NULL DEFAULT true; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231130184144_uptime_kb_config/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Config" ADD COLUMN "client_version" TEXT, 3 | ADD COLUMN "feature_previews" BOOLEAN NOT NULL DEFAULT false, 4 | ADD COLUMN "gh_version" TEXT, 5 | ADD COLUMN "out_of_office" BOOLEAN NOT NULL DEFAULT false, 6 | ADD COLUMN "out_of_office_end" TIMESTAMP(3), 7 | ADD COLUMN "out_of_office_message" TEXT, 8 | ADD COLUMN "out_of_office_start" TIMESTAMP(3), 9 | ADD COLUMN "portal_locale" TEXT, 10 | ADD COLUMN "sso_active" BOOLEAN NOT NULL DEFAULT false, 11 | ADD COLUMN "sso_provider" TEXT; 12 | 13 | -- CreateTable 14 | CREATE TABLE "SSO" ( 15 | "id" TEXT NOT NULL, 16 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 18 | "name" TEXT NOT NULL, 19 | "clientId" TEXT NOT NULL, 20 | "clientSecret" TEXT NOT NULL, 21 | "active" BOOLEAN NOT NULL, 22 | "issuer" TEXT, 23 | "tenantId" TEXT, 24 | 25 | CONSTRAINT "SSO_pkey" PRIMARY KEY ("id") 26 | ); 27 | 28 | -- CreateTable 29 | CREATE TABLE "Uptime" ( 30 | "id" TEXT NOT NULL, 31 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 32 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 33 | "name" TEXT NOT NULL, 34 | "url" TEXT NOT NULL, 35 | "active" BOOLEAN NOT NULL DEFAULT false, 36 | "webhook" TEXT, 37 | "latency" INTEGER, 38 | "status" BOOLEAN, 39 | 40 | CONSTRAINT "Uptime_pkey" PRIMARY KEY ("id") 41 | ); 42 | 43 | -- CreateTable 44 | CREATE TABLE "knowledgeBase" ( 45 | "id" TEXT NOT NULL, 46 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 47 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 48 | "title" TEXT NOT NULL, 49 | "content" TEXT NOT NULL, 50 | "tags" TEXT[], 51 | "author" TEXT NOT NULL, 52 | "public" BOOLEAN NOT NULL DEFAULT false, 53 | "ticketId" TEXT, 54 | 55 | CONSTRAINT "knowledgeBase_pkey" PRIMARY KEY ("id") 56 | ); 57 | 58 | -- AddForeignKey 59 | ALTER TABLE "knowledgeBase" ADD CONSTRAINT "knowledgeBase_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "Ticket"("id") ON DELETE SET NULL ON UPDATE CASCADE; 60 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231130184716_sso/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `SSO` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropTable 8 | DROP TABLE "SSO"; 9 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231130185305_allow/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Config" ALTER COLUMN "notifications" DROP NOT NULL; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231201011858_redirect/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Account` table. If the table is not empty, all the data it contains will be lost. 5 | - You are about to drop the `VerificationToken` table. If the table is not empty, all the data it contains will be lost. 6 | 7 | */ 8 | -- DropForeignKey 9 | ALTER TABLE "Account" DROP CONSTRAINT "Account_userId_fkey"; 10 | 11 | -- AlterTable 12 | ALTER TABLE "Provider" ADD COLUMN "redirectUri" TEXT; 13 | 14 | -- DropTable 15 | DROP TABLE "Account"; 16 | 17 | -- DropTable 18 | DROP TABLE "VerificationToken"; 19 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231202030625_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Config" ADD COLUMN "encryption_key" TEXT; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231202030940_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Config" ADD COLUMN "first_time_setup" BOOLEAN NOT NULL DEFAULT true; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231202031821_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The `encryption_key` column on the `Config` table would be dropped and recreated. This will lead to data loss if there is data in the column. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Config" DROP COLUMN "encryption_key", 9 | ADD COLUMN "encryption_key" BYTEA; 10 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231203164241_/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "Comment" DROP CONSTRAINT "Comment_userId_fkey"; 3 | 4 | -- AlterTable 5 | ALTER TABLE "Comment" ALTER COLUMN "userId" DROP NOT NULL; 6 | 7 | -- AddForeignKey 8 | ALTER TABLE "Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 9 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231203224035_code/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "PasswordResetToken" ( 3 | "id" TEXT NOT NULL, 4 | "code" TEXT NOT NULL, 5 | "userId" TEXT NOT NULL, 6 | 7 | CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id") 8 | ); 9 | 10 | -- CreateIndex 11 | CREATE UNIQUE INDEX "PasswordResetToken_code_key" ON "PasswordResetToken"("code"); 12 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231206001238_interanl_notifications/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "notifications" ( 3 | "id" TEXT NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | "title" TEXT NOT NULL, 7 | "read" BOOLEAN NOT NULL DEFAULT false, 8 | "text" TEXT NOT NULL, 9 | "userId" TEXT NOT NULL, 10 | 11 | CONSTRAINT "notifications_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- AddForeignKey 15 | ALTER TABLE "notifications" ADD CONSTRAINT "notifications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 16 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20231206002327_remove_title/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `title` on the `notifications` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "notifications" DROP COLUMN "title"; 9 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20240322235917_templates/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "Template" AS ENUM ('ticket_created', 'ticket_status_changed', 'ticket_assigned', 'ticket_comment'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "emailTemplate" ( 6 | "id" TEXT NOT NULL, 7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "name" TEXT NOT NULL, 10 | "body" TEXT NOT NULL, 11 | "type" "Template" NOT NULL, 12 | 13 | CONSTRAINT "emailTemplate_pkey" PRIMARY KEY ("id") 14 | ); 15 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20240323000015_emailtemplate_recock/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `body` on the `emailTemplate` table. All the data in the column will be lost. 5 | - You are about to drop the column `name` on the `emailTemplate` table. All the data in the column will be lost. 6 | - Added the required column `html` to the `emailTemplate` table without a default value. This is not possible if the table is not empty. 7 | 8 | */ 9 | -- AlterTable 10 | ALTER TABLE "emailTemplate" DROP COLUMN "body", 11 | DROP COLUMN "name", 12 | ADD COLUMN "html" TEXT NOT NULL; 13 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20240325230914_external_user/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "external_user" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20240330132748_storage/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `encoding` to the `TicketFile` table without a default value. This is not possible if the table is not empty. 5 | - Added the required column `mime` to the `TicketFile` table without a default value. This is not possible if the table is not empty. 6 | - Added the required column `size` to the `TicketFile` table without a default value. This is not possible if the table is not empty. 7 | - Added the required column `userId` to the `TicketFile` table without a default value. This is not possible if the table is not empty. 8 | 9 | */ 10 | -- AlterTable 11 | ALTER TABLE "TicketFile" ADD COLUMN "encoding" TEXT NOT NULL, 12 | ADD COLUMN "mime" TEXT NOT NULL, 13 | ADD COLUMN "size" INTEGER NOT NULL, 14 | ADD COLUMN "userId" TEXT NOT NULL; 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "TicketFile" ADD CONSTRAINT "TicketFile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20240330224718_notifications/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "notifications" ADD COLUMN "ticketId" TEXT; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "notifications" ADD CONSTRAINT "notifications_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "Ticket"("id") ON DELETE SET NULL ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20240531225221_createdby/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Ticket" ADD COLUMN "createdBy" JSONB; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241014201131_openid/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "openIdConfig" ( 3 | "id" SERIAL NOT NULL, 4 | "clientId" TEXT NOT NULL, 5 | "clientSecret" TEXT NOT NULL, 6 | "issuer" TEXT NOT NULL, 7 | "redirectUri" TEXT NOT NULL, 8 | "jwtSecret" TEXT NOT NULL, 9 | 10 | CONSTRAINT "openIdConfig_pkey" PRIMARY KEY ("id") 11 | ); 12 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241014203742_authentication/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Provider` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropTable 8 | DROP TABLE "Provider"; 9 | 10 | -- CreateTable 11 | CREATE TABLE "OAuthProvider" ( 12 | "id" SERIAL NOT NULL, 13 | "name" TEXT NOT NULL, 14 | "clientId" TEXT NOT NULL, 15 | "clientSecret" TEXT NOT NULL, 16 | "authorizationUrl" TEXT NOT NULL, 17 | "tokenUrl" TEXT NOT NULL, 18 | "userInfoUrl" TEXT NOT NULL, 19 | "redirectUri" TEXT NOT NULL, 20 | "scope" TEXT NOT NULL, 21 | 22 | CONSTRAINT "OAuthProvider_pkey" PRIMARY KEY ("id") 23 | ); 24 | 25 | -- CreateTable 26 | CREATE TABLE "SAMLProvider" ( 27 | "id" SERIAL NOT NULL, 28 | "name" TEXT NOT NULL, 29 | "entryPoint" TEXT NOT NULL, 30 | "issuer" TEXT NOT NULL, 31 | "cert" TEXT NOT NULL, 32 | "ssoLoginUrl" TEXT NOT NULL, 33 | "ssoLogoutUrl" TEXT NOT NULL, 34 | "audience" TEXT NOT NULL, 35 | "recipient" TEXT NOT NULL, 36 | "destination" TEXT NOT NULL, 37 | "acsUrl" TEXT NOT NULL, 38 | 39 | CONSTRAINT "SAMLProvider_pkey" PRIMARY KEY ("id") 40 | ); 41 | 42 | -- CreateIndex 43 | CREATE UNIQUE INDEX "OAuthProvider_name_key" ON "OAuthProvider"("name"); 44 | 45 | -- CreateIndex 46 | CREATE UNIQUE INDEX "SAMLProvider_name_key" ON "SAMLProvider"("name"); 47 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241014210350_cleanup/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `out_of_office` on the `Config` table. All the data in the column will be lost. 5 | - You are about to drop the column `out_of_office_end` on the `Config` table. All the data in the column will be lost. 6 | - You are about to drop the column `out_of_office_message` on the `Config` table. All the data in the column will be lost. 7 | - You are about to drop the column `out_of_office_start` on the `Config` table. All the data in the column will be lost. 8 | - You are about to drop the column `portal_locale` on the `Config` table. All the data in the column will be lost. 9 | 10 | */ 11 | -- AlterTable 12 | ALTER TABLE "Config" DROP COLUMN "out_of_office", 13 | DROP COLUMN "out_of_office_end", 14 | DROP COLUMN "out_of_office_message", 15 | DROP COLUMN "out_of_office_start", 16 | DROP COLUMN "portal_locale"; 17 | 18 | -- AlterTable 19 | ALTER TABLE "User" ADD COLUMN "out_of_office" BOOLEAN NOT NULL DEFAULT false, 20 | ADD COLUMN "out_of_office_end" TIMESTAMP(3), 21 | ADD COLUMN "out_of_office_message" TEXT, 22 | ADD COLUMN "out_of_office_start" TIMESTAMP(3); 23 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241018192053_drop_col/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `clientSecret` on the `openIdConfig` table. All the data in the column will be lost. 5 | - You are about to drop the column `jwtSecret` on the `openIdConfig` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "openIdConfig" DROP COLUMN "clientSecret", 10 | DROP COLUMN "jwtSecret"; 11 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241020165331_oauth_email_authentication/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Email" ADD COLUMN "clientId" TEXT, 3 | ADD COLUMN "clientSecret" TEXT, 4 | ADD COLUMN "refreshToken" TEXT, 5 | ADD COLUMN "serviceType" TEXT NOT NULL DEFAULT 'other', 6 | ADD COLUMN "tenantId" TEXT, 7 | ALTER COLUMN "pass" DROP NOT NULL; 8 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241021215357_accesstokensmtp/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Email" ADD COLUMN "accessToken" TEXT, 3 | ADD COLUMN "expiresIn" INTEGER; 4 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241021220058_bigint/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Email" ALTER COLUMN "expiresIn" SET DATA TYPE BIGINT; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241021230158_redirecturi/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Email" ADD COLUMN "redirectUri" TEXT, 3 | ALTER COLUMN "expiresIn" DROP NOT NULL; 4 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241102020326_replyto_support/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Comment" ADD COLUMN "reply" BOOLEAN NOT NULL DEFAULT false, 3 | ADD COLUMN "replyEmail" TEXT; 4 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241104215009_imap_oauth/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "EmailQueue" ADD COLUMN "accessToken" TEXT, 3 | ADD COLUMN "active" BOOLEAN NOT NULL DEFAULT true, 4 | ADD COLUMN "clientId" TEXT, 5 | ADD COLUMN "clientSecret" TEXT, 6 | ADD COLUMN "expiresIn" BIGINT, 7 | ADD COLUMN "redirectUri" TEXT, 8 | ADD COLUMN "refreshToken" TEXT, 9 | ADD COLUMN "serviceType" TEXT NOT NULL DEFAULT 'other', 10 | ADD COLUMN "tenantId" TEXT; 11 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241106185045_imap_password_optional/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "EmailQueue" ALTER COLUMN "password" DROP NOT NULL; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241106233134_lock/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Ticket" ADD COLUMN "locked" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241106234810_cascade/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "Comment" DROP CONSTRAINT "Comment_ticketId_fkey"; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "Comment" ADD CONSTRAINT "Comment_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "Ticket"("id") ON DELETE CASCADE ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241111235707_edited_comment/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Comment" ADD COLUMN "edited" BOOLEAN NOT NULL DEFAULT false, 3 | ADD COLUMN "editedAt" TIMESTAMP(3), 4 | ADD COLUMN "previous" TEXT; 5 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241113193825_rbac/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "Permission" AS ENUM ('CREATE_TICKET', 'READ_TICKET', 'WRITE_TICKET', 'UPDATE_TICKET', 'DELETE_TICKET', 'ASSIGN_TICKET', 'TRANSFER_TICKET', 'COMMENT_TICKET', 'CREATE_USER', 'READ_USER', 'UPDATE_USER', 'DELETE_USER', 'MANAGE_USERS', 'CREATE_ROLE', 'READ_ROLE', 'UPDATE_ROLE', 'DELETE_ROLE', 'MANAGE_ROLES', 'CREATE_TEAM', 'READ_TEAM', 'UPDATE_TEAM', 'DELETE_TEAM', 'MANAGE_TEAMS', 'CREATE_CLIENT', 'READ_CLIENT', 'UPDATE_CLIENT', 'DELETE_CLIENT', 'MANAGE_CLIENTS', 'CREATE_KB', 'READ_KB', 'UPDATE_KB', 'DELETE_KB', 'MANAGE_KB', 'VIEW_REPORTS', 'MANAGE_SETTINGS', 'MANAGE_WEBHOOKS', 'MANAGE_INTEGRATIONS', 'MANAGE_EMAIL_TEMPLATES', 'CREATE_TIME_ENTRY', 'READ_TIME_ENTRY', 'UPDATE_TIME_ENTRY', 'DELETE_TIME_ENTRY', 'MANAGE_DOCS', 'ADMIN_PANEL'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "Role" ( 6 | "id" TEXT NOT NULL, 7 | "name" TEXT NOT NULL, 8 | "description" TEXT, 9 | "permissions" "Permission"[], 10 | "isDefault" BOOLEAN NOT NULL DEFAULT false, 11 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 | "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 13 | 14 | CONSTRAINT "Role_pkey" PRIMARY KEY ("id") 15 | ); 16 | 17 | -- CreateTable 18 | CREATE TABLE "_RoleToUser" ( 19 | "A" TEXT NOT NULL, 20 | "B" TEXT NOT NULL 21 | ); 22 | 23 | -- CreateIndex 24 | CREATE UNIQUE INDEX "Role_name_key" ON "Role"("name"); 25 | 26 | -- CreateIndex 27 | CREATE UNIQUE INDEX "_RoleToUser_AB_unique" ON "_RoleToUser"("A", "B"); 28 | 29 | -- CreateIndex 30 | CREATE INDEX "_RoleToUser_B_index" ON "_RoleToUser"("B"); 31 | 32 | -- AddForeignKey 33 | ALTER TABLE "_RoleToUser" ADD CONSTRAINT "_RoleToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE; 34 | 35 | -- AddForeignKey 36 | ALTER TABLE "_RoleToUser" ADD CONSTRAINT "_RoleToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 37 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241113195413_role_active/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Config" ADD COLUMN "roles_active" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241113200612_update_types/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The `permissions` column on the `Role` table would be dropped and recreated. This will lead to data loss if there is data in the column. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Role" DROP COLUMN "permissions", 9 | ADD COLUMN "permissions" JSONB[]; 10 | 11 | -- DropEnum 12 | DROP TYPE "Permission"; 13 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241114164206_sessions/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Session" ADD COLUMN "apiKey" BOOLEAN NOT NULL DEFAULT false, 3 | ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 4 | ADD COLUMN "ipAddress" TEXT, 5 | ADD COLUMN "userAgent" TEXT; 6 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241115235800_following/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Ticket" ADD COLUMN "following" JSONB; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/20241116014522_hold/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterEnum 2 | ALTER TYPE "TicketStatus" ADD VALUE 'hold'; 3 | -------------------------------------------------------------------------------- /apps/api/src/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /apps/api/src/routes.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from "fastify"; 2 | import { authRoutes } from "./controllers/auth"; 3 | import { clientRoutes } from "./controllers/clients"; 4 | import { configRoutes } from "./controllers/config"; 5 | import { dataRoutes } from "./controllers/data"; 6 | import { notebookRoutes } from "./controllers/notebook"; 7 | import { emailQueueRoutes } from "./controllers/queue"; 8 | import { roleRoutes } from "./controllers/roles"; 9 | import { objectStoreRoutes } from "./controllers/storage"; 10 | import { ticketRoutes } from "./controllers/ticket"; 11 | import { timeTrackingRoutes } from "./controllers/time"; 12 | import { userRoutes } from "./controllers/users"; 13 | import { webhookRoutes } from "./controllers/webhooks"; 14 | 15 | export function registerRoutes(fastify: FastifyInstance) { 16 | authRoutes(fastify); 17 | emailQueueRoutes(fastify); 18 | dataRoutes(fastify); 19 | ticketRoutes(fastify); 20 | userRoutes(fastify); 21 | notebookRoutes(fastify); 22 | clientRoutes(fastify); 23 | webhookRoutes(fastify); 24 | configRoutes(fastify); 25 | timeTrackingRoutes(fastify); 26 | objectStoreRoutes(fastify); 27 | roleRoutes(fastify); 28 | } 29 | -------------------------------------------------------------------------------- /apps/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "commonjs", 5 | "lib": ["ES7", "ESNext"], 6 | "rootDir": "src", 7 | "declaration": true, 8 | "outDir": "dist", 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true 16 | }, 17 | "include": ["."], 18 | "exclude": ["dist", "build", "node_modules"] 19 | } 20 | -------------------------------------------------------------------------------- /apps/client/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | npm-debug.log 5 | README.md 6 | .next 7 | static 8 | -------------------------------------------------------------------------------- /apps/client/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CLIENT_VERSION="0.5.4.2" -------------------------------------------------------------------------------- /apps/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | 39 | /.DS_Store 40 | ./.DS_Store 41 | 42 | /storage 43 | 44 | /public/*.js 45 | 46 | /public/*.js 47 | /public/*js.map -------------------------------------------------------------------------------- /apps/client/@/shadcn/components/forbidden.tsx: -------------------------------------------------------------------------------- 1 | import { toast } from "../hooks/use-toast"; 2 | 3 | const NoPermissions = () => { 4 | toast({ 5 | title: "Unauthorized", 6 | description: "Please check your permissions.", 7 | }); 8 | return ( 9 |
10 |
11 |

Access Denied

12 |

13 | You do not have permission to view this page. 14 |

15 |
16 |
17 | ); 18 | }; 19 | 20 | export default NoPermissions; 21 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/components/nav-projects.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Folder, 3 | Forward, 4 | MoreHorizontal, 5 | Trash2, 6 | type LucideIcon, 7 | } from "lucide-react" 8 | 9 | import { 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuSeparator, 14 | DropdownMenuTrigger, 15 | } from "@/shadcn/ui/dropdown-menu"; 16 | import { 17 | SidebarGroup, 18 | SidebarGroupLabel, 19 | SidebarMenu, 20 | SidebarMenuAction, 21 | SidebarMenuButton, 22 | SidebarMenuItem, 23 | useSidebar, 24 | } from "@/shadcn/ui/sidebar"; 25 | 26 | export function NavProjects({ 27 | projects, 28 | }: { 29 | projects: { 30 | name: string 31 | url: string 32 | icon: LucideIcon 33 | }[] 34 | }) { 35 | const { isMobile } = useSidebar() 36 | 37 | return ( 38 | 39 | Projects 40 | 41 | {projects.map((item) => ( 42 | 43 | 44 | 45 | 46 | {item.name} 47 | 48 | 49 | 50 | 51 | 52 | 53 | More 54 | 55 | 56 | 61 | 62 | 63 | View Project 64 | 65 | 66 | 67 | Share Project 68 | 69 | 70 | 71 | 72 | Delete Project 73 | 74 | 75 | 76 | 77 | ))} 78 | 79 | 80 | 81 | More 82 | 83 | 84 | 85 | 86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/components/tickets/DisplaySettings.tsx: -------------------------------------------------------------------------------- 1 | import { Label } from "@/shadcn/ui/label"; 2 | import { Switch } from "@/shadcn/ui/switch"; 3 | import { UISettings } from "../../types/tickets"; 4 | 5 | interface DisplaySettingsProps { 6 | settings: UISettings; 7 | onChange: (setting: keyof UISettings, value: boolean) => void; 8 | } 9 | 10 | export default function DisplaySettings({ settings, onChange }: DisplaySettingsProps) { 11 | return ( 12 |
13 |

Display Options

14 |
15 |
16 | 17 | onChange('showAvatars', checked)} 21 | /> 22 |
23 | 24 |
25 | 26 | onChange('showDates', checked)} 30 | /> 31 |
32 | 33 |
34 | 35 | onChange('showPriority', checked)} 39 | /> 40 |
41 | 42 |
43 | 44 | onChange('showType', checked)} 48 | /> 49 |
50 | 51 |
52 | 53 | onChange('showTicketNumbers', checked)} 57 | /> 58 |
59 |
60 |
61 | ); 62 | } -------------------------------------------------------------------------------- /apps/client/@/shadcn/components/tickets/FilterBadge.tsx: -------------------------------------------------------------------------------- 1 | import { X } from "lucide-react"; 2 | 3 | interface FilterBadgeProps { 4 | text: string; 5 | onRemove: () => void; 6 | } 7 | 8 | export default function FilterBadge({ text, onRemove }: FilterBadgeProps) { 9 | return ( 10 |
11 | {text} 12 | 21 |
22 | ); 23 | } -------------------------------------------------------------------------------- /apps/client/@/shadcn/hooks/use-media-query.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | export function useMediaQuery(query: string) { 4 | const [value, setValue] = React.useState(false) 5 | 6 | React.useEffect(() => { 7 | function onChange(event: MediaQueryListEvent) { 8 | setValue(event.matches) 9 | } 10 | 11 | const result = matchMedia(query) 12 | result.addEventListener("change", onChange) 13 | setValue(result.matches) 14 | 15 | return () => result.removeEventListener("change", onChange) 16 | }, [query]) 17 | 18 | return value 19 | } -------------------------------------------------------------------------------- /apps/client/@/shadcn/hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/hooks/useTicketFilters.ts: -------------------------------------------------------------------------------- 1 | import { Ticket } from '@/shadcn/types/tickets'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | export function useTicketFilters(tickets: Ticket[] = []) { 5 | const [selectedPriorities, setSelectedPriorities] = useState(() => { 6 | const saved = localStorage.getItem("all_selectedPriorities"); 7 | return saved ? JSON.parse(saved) : []; 8 | }); 9 | 10 | const [selectedStatuses, setSelectedStatuses] = useState(() => { 11 | const saved = localStorage.getItem("all_selectedStatuses"); 12 | return saved ? JSON.parse(saved) : []; 13 | }); 14 | 15 | const [selectedAssignees, setSelectedAssignees] = useState(() => { 16 | const saved = localStorage.getItem("all_selectedAssignees"); 17 | return saved ? JSON.parse(saved) : []; 18 | }); 19 | 20 | useEffect(() => { 21 | localStorage.setItem("all_selectedPriorities", JSON.stringify(selectedPriorities)); 22 | localStorage.setItem("all_selectedStatuses", JSON.stringify(selectedStatuses)); 23 | localStorage.setItem("all_selectedAssignees", JSON.stringify(selectedAssignees)); 24 | }, [selectedPriorities, selectedStatuses, selectedAssignees]); 25 | 26 | const handlePriorityToggle = (priority: string) => { 27 | setSelectedPriorities((prev) => 28 | prev.includes(priority) 29 | ? prev.filter((p) => p !== priority) 30 | : [...prev, priority] 31 | ); 32 | }; 33 | 34 | const handleStatusToggle = (status: string) => { 35 | setSelectedStatuses((prev) => 36 | prev.includes(status) 37 | ? prev.filter((s) => s !== status) 38 | : [...prev, status] 39 | ); 40 | }; 41 | 42 | const handleAssigneeToggle = (assignee: string) => { 43 | setSelectedAssignees((prev) => 44 | prev.includes(assignee) 45 | ? prev.filter((a) => a !== assignee) 46 | : [...prev, assignee] 47 | ); 48 | }; 49 | 50 | const clearFilters = () => { 51 | setSelectedPriorities([]); 52 | setSelectedStatuses([]); 53 | setSelectedAssignees([]); 54 | }; 55 | 56 | const filteredTickets = tickets.filter((ticket) => { 57 | const priorityMatch = 58 | selectedPriorities.length === 0 || 59 | selectedPriorities.includes(ticket.priority); 60 | const statusMatch = 61 | selectedStatuses.length === 0 || 62 | selectedStatuses.includes(ticket.isComplete ? "closed" : "open"); 63 | const assigneeMatch = 64 | selectedAssignees.length === 0 || 65 | selectedAssignees.includes(ticket.assignedTo?.name || "Unassigned"); 66 | 67 | return priorityMatch && statusMatch && assigneeMatch; 68 | }); 69 | 70 | return { 71 | selectedPriorities, 72 | selectedStatuses, 73 | selectedAssignees, 74 | handlePriorityToggle, 75 | handleStatusToggle, 76 | handleAssigneeToggle, 77 | clearFilters, 78 | filteredTickets 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/lib/hasAccess.ts: -------------------------------------------------------------------------------- 1 | import { toast } from "@/shadcn/hooks/use-toast"; 2 | 3 | export const hasAccess = (response: Response) => { 4 | if (response.status === 401) { 5 | toast({ 6 | title: "Unauthorized", 7 | description: "Please check your permissions.", 8 | }); 9 | } 10 | return response; 11 | }; 12 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/lib/types/email.ts: -------------------------------------------------------------------------------- 1 | export interface EmailConfig { 2 | user: string; 3 | host: string; 4 | port: number; 5 | tls: boolean; 6 | tlsOptions: { 7 | rejectUnauthorized: boolean; 8 | servername: string; 9 | }; 10 | xoauth2?: string; 11 | password?: string; 12 | } 13 | 14 | export type EmailQueue = { 15 | serviceType: "gmail" | "other"; 16 | id: string; 17 | username: string; 18 | hostname: string; 19 | password?: string; 20 | clientId?: string; 21 | clientSecret?: string; 22 | refreshToken?: string; 23 | accessToken?: string; 24 | expiresIn?: number; 25 | tls?: boolean; 26 | }; 27 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | 8 | export function classNames(...classes: any) { 9 | return classes.filter(Boolean).join(" "); 10 | } -------------------------------------------------------------------------------- /apps/client/@/shadcn/types/tickets.ts: -------------------------------------------------------------------------------- 1 | export type ViewMode = 'list' | 'kanban'; 2 | export type KanbanGrouping = 'status' | 'priority' | 'type' | 'assignee'; 3 | export type SortOption = 'newest' | 'oldest' | 'priority' | 'title'; 4 | 5 | export type Team = { 6 | id: string; 7 | name: string; 8 | }; 9 | 10 | export type User = { 11 | id: string; 12 | name: string; 13 | }; 14 | 15 | export type Ticket = { 16 | id: string; 17 | Number: number; 18 | title: string; 19 | priority: string; 20 | type: string; 21 | status: string; 22 | createdAt: string; 23 | team?: Team; 24 | assignedTo?: User; 25 | isComplete: boolean; 26 | }; 27 | 28 | export type KanbanColumn = { 29 | id: string; 30 | title: string; 31 | color: string; 32 | tickets: Ticket[]; 33 | }; 34 | 35 | export interface UISettings { 36 | showAvatars: boolean; 37 | showDates: boolean; 38 | showPriority: boolean; 39 | showType: boolean; 40 | showTicketNumbers: boolean; 41 | } -------------------------------------------------------------------------------- /apps/client/@/shadcn/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 3 | 4 | import { cn } from "@/shadcn/lib/utils" 5 | 6 | const Avatar = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | )) 19 | Avatar.displayName = AvatarPrimitive.Root.displayName 20 | 21 | const AvatarImage = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 30 | )) 31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 32 | 33 | const AvatarFallback = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | )) 46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 47 | 48 | export { Avatar, AvatarImage, AvatarFallback } 49 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "../lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/shadcn/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 2 | 3 | const Collapsible = CollapsiblePrimitive.Root 4 | 5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 6 | 7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 8 | 9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 10 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/shadcn/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/shadcn/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as PopoverPrimitive from "@radix-ui/react-popover" 3 | 4 | import { cn } from "@/shadcn/lib/utils" 5 | 6 | const Popover = PopoverPrimitive.Root 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger 9 | 10 | const PopoverContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 | 15 | 25 | 26 | )) 27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 28 | 29 | export { Popover, PopoverTrigger, PopoverContent } 30 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 3 | 4 | import { cn } from "@/shadcn/lib/utils" 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = "horizontal", decorative = true, ...props }, 12 | ref 13 | ) => ( 14 | 25 | ) 26 | ) 27 | Separator.displayName = SeparatorPrimitive.Root.displayName 28 | 29 | export { Separator } 30 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/shadcn/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SwitchPrimitives from "@radix-ui/react-switch" 3 | 4 | import { cn } from "@/shadcn/lib/utils" 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 23 | 24 | )) 25 | Switch.displayName = SwitchPrimitives.Root.displayName 26 | 27 | export { Switch } 28 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | import { useToast } from "@/shadcn//hooks/use-toast" 2 | import { 3 | Toast, 4 | ToastClose, 5 | ToastDescription, 6 | ToastProvider, 7 | ToastTitle, 8 | ToastViewport, 9 | } from "@/shadcn//ui/toast" 10 | 11 | export function Toaster() { 12 | const { toasts } = useToast() 13 | 14 | return ( 15 | 16 | {toasts.map(function ({ id, title, description, action, ...props }) { 17 | return ( 18 | 19 |
20 | {title && {title}} 21 | {description && ( 22 | {description} 23 | )} 24 |
25 | {action} 26 | 27 |
28 | ) 29 | })} 30 | 31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /apps/client/@/shadcn/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 3 | 4 | import { cn } from "@/shadcn/lib/utils" 5 | 6 | const TooltipProvider = TooltipPrimitive.Provider 7 | 8 | const Tooltip = TooltipPrimitive.Root 9 | 10 | const TooltipTrigger = TooltipPrimitive.Trigger 11 | 12 | const TooltipContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, sideOffset = 4, ...props }, ref) => ( 16 | 25 | )) 26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 27 | 28 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 29 | -------------------------------------------------------------------------------- /apps/client/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "styles/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/shadcn/", 15 | "utils": "@/shadcn/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /apps/client/components/BlockEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCreateBlockNote } from "@blocknote/react"; 2 | import { BlockNoteView } from "@blocknote/mantine"; 3 | 4 | 5 | import "@blocknote/core/fonts/inter.css"; 6 | import "@blocknote/mantine/style.css"; 7 | 8 | export default function BlockNoteEditor({ setIssue }) { 9 | const editor = useCreateBlockNote(); 10 | 11 | return ( 12 | { 18 | setIssue(editor.document); 19 | }} 20 | /> 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /apps/client/components/CreateEmailQueue/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/apps/client/components/CreateEmailQueue/index.js -------------------------------------------------------------------------------- /apps/client/components/ListUserFiles/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import fileDownload from "js-file-download"; 3 | import axios from "axios"; 4 | import { TrashIcon, DocumentDownloadIcon } from "@heroicons/react/solid"; 5 | 6 | export default function ListUserFiles({ uploaded, setUploaded }) { 7 | const [files, setFiles] = useState([]); 8 | 9 | async function getFiles() { 10 | await fetch(`/api/v1/users/file/all`, { 11 | method: "GET", 12 | headers: { 13 | "Content-Type": "application/json", 14 | }, 15 | }) 16 | .then((res) => res.json()) 17 | .then((res) => { 18 | setFiles(res.files); 19 | setUploaded(false); 20 | }); 21 | } 22 | 23 | async function deleteFile(file) { 24 | await fetch(`/api/v1/users/file/delete`, { 25 | method: "DELETE", 26 | headers: { 27 | "Content-Type": "application/json", 28 | }, 29 | body: JSON.stringify({ 30 | id: file.id, 31 | path: file.path, 32 | }), 33 | }) 34 | .then((res) => res.json()) 35 | .then(() => { 36 | getFiles(); 37 | }); 38 | } 39 | 40 | function download(file) { 41 | const url = `/api/v1/users/file/download?id=${file.id}`; 42 | let data = new FormData(); 43 | axios 44 | .post(url, data, { 45 | headers: { 46 | "Content-Type": "application/json", 47 | }, 48 | responseType: "blob", 49 | }) 50 | .then((res) => { 51 | fileDownload(res.data, file.filename); 52 | }); 53 | } 54 | 55 | useEffect(() => { 56 | getFiles(); 57 | }, [uploaded]); 58 | 59 | return ( 60 |
61 |
62 | {files ? ( 63 | files.map((file) => { 64 | return ( 65 |
66 |
    67 |
  • 68 | {file.filename} 69 | 79 | 86 |
  • 87 |
88 |
89 | ); 90 | }) 91 | ) : ( 92 |

No files found

93 | )} 94 |
95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /apps/client/components/ThemeSettings/index.tsx: -------------------------------------------------------------------------------- 1 | import { getCookie } from "cookies-next"; 2 | import { useEffect, useState } from "react"; 3 | import { useUser } from "../../store/session"; 4 | import { 5 | Select, 6 | SelectContent, 7 | SelectGroup, 8 | SelectItem, 9 | SelectLabel, 10 | SelectTrigger, 11 | SelectValue, 12 | } from "@/shadcn/ui/select"; 13 | import { useSidebar } from "@/shadcn/ui/sidebar"; 14 | import { Sun } from "lucide-react"; 15 | import { Moon } from "lucide-react"; 16 | 17 | export default function ThemeSettings() { 18 | const { user } = useUser(); 19 | const token = getCookie("session"); 20 | 21 | const [theme, setTheme] = useState(""); 22 | 23 | const { state } = useSidebar(); 24 | 25 | useEffect(() => { 26 | // Retrieve the saved theme from localStorage or default to 'light' 27 | const savedTheme = localStorage.getItem("theme") || "light"; 28 | setTheme(savedTheme); 29 | document.documentElement.className = savedTheme; 30 | }, []); 31 | 32 | const toggleTheme = (selectedTheme) => { 33 | // Update the class on the root element 34 | document.documentElement.className = selectedTheme; 35 | // Update state and save the theme in localStorage 36 | setTheme(selectedTheme); 37 | localStorage.setItem("theme", selectedTheme); 38 | }; 39 | 40 | return ( 41 |
42 |
43 |
44 | 71 |
72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /apps/client/i18n.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | locales: ["en", "da", "de", "es", "fr", "no", "pt", "se", "tl", "is" ,"it", "he", "tr", "hu", "th", "zh-CN"], 3 | defaultLocale: "en", 4 | pages: { 5 | "*": ["peppermint"], 6 | }, 7 | localeDetection: true, 8 | }; 9 | -------------------------------------------------------------------------------- /apps/client/lib/cookie/index.js: -------------------------------------------------------------------------------- 1 | // FILE TO PERFORM SEVERAL FUNCTIONS RELATED TO COOKIES 2 | // ============================================================================= 3 | // 4 | // Check if cookie exists 5 | // Check if cookie is valid 6 | // Revalidate cookie if expired 7 | // return true or false 8 | // function to return session token 9 | 10 | import { getCookie } from 'cookies-next'; 11 | 12 | 13 | export const checkCookieExists = () => { 14 | 15 | const access = getCookie('session'); 16 | 17 | } -------------------------------------------------------------------------------- /apps/client/locales/da/peppermint.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello_good": "Godt", 3 | "hello_morning": "Morgen", 4 | "hello_afternoon": "Eftermiddag", 5 | "sl_dashboard": "Instrumentbræt", 6 | "sl_tickets": "Billetter", 7 | "sl_history": "Historie", 8 | "sl_notebook": "Personlig Notesbog", 9 | "sl_users": "Brugere", 10 | "sl_clients": "Kunder", 11 | "sl_settings": "Indstillinger", 12 | "open_tickets": "Åbne Billetter", 13 | "completed_tickets": "Afsluttede Billetter", 14 | "account_status": "Kontostatus", 15 | "todo_list": "Opgaveliste", 16 | "personal_files": "Personlige Filer", 17 | "create_ticket": "Opret Billet", 18 | "ticket_new": "Ny Billet", 19 | "ticket_name_here": "Navn", 20 | "ticket_email_here": "Email", 21 | "ticket_details": "Titel", 22 | "ticket_select_client": "Vælg en Klient", 23 | "ticket_select_eng": "Vælg en Ingeniør", 24 | "ticket_extra_details": "Indtast ekstra detaljer her... understøtter markdown", 25 | "cancel": "Annuller", 26 | "create": "Opret", 27 | "low": "Lav", 28 | "normal": "Normal", 29 | "high": "Høj", 30 | "edit-btn": "Rediger", 31 | "reset_password": "Nulstil Adgangskode", 32 | "internal_users": "Interne Brugere", 33 | "clients": "Kunder", 34 | "new_user": "Ny Bruger", 35 | "new_client": "Ny Klient", 36 | "previous": "Forrige", 37 | "next": "Næste", 38 | "show": "Vis", 39 | "name": "Navn", 40 | "email": "Email", 41 | "settings": "Indstillinger", 42 | "webhooks": "Webhooks", 43 | "version": "Version", 44 | "v_profile": "Se Profil", 45 | "reminders": "Påmindelser", 46 | "title": "Titel", 47 | "priority": "Prioritet", 48 | "status": "Status", 49 | "created": "Oprettet", 50 | "assigned_to": "Tildelt Til", 51 | "enter_todo": "Indtast Opgave", 52 | "notebooks": "Notesbøger", 53 | "notebooks_description": "Dette er en personlig notesbog. Her kan du gemme noter, links, kodeudsnit osv.", 54 | "create_notebook": "Opret Notesbog", 55 | "open": "Åben", 56 | "assigned_to_me": "Tildelt til Mig", 57 | "unassigned": "Ikke Tildelt", 58 | "closed": "Lukket", 59 | "description": "Beskrivelse", 60 | "comments": "Kommentarer", 61 | "leave_comment": "Efterlad en Kommentar", 62 | "close_issue": "Luk Problem", 63 | "comment": "Kommentar", 64 | "save": "Gem", 65 | "labels": "Mærker", 66 | "created_at": "Oprettet den", 67 | "updated_at": "Opdateret den", 68 | "hide_ticket": "Skjul Billet", 69 | "show_ticket": "Vis Globalt", 70 | "open_issue": "Åbn Problem", 71 | "closed_issue": "Lukket Problem", 72 | "recent_tickets": "Seneste Billetter", 73 | "notebook_title": "Notesbogstitel", 74 | "admin_settings": "Administratorindstillinger", 75 | "profile": "Profil", 76 | "logout": "Log Ud", 77 | "unassigned_tickets": "Ikke Tildelte Billetter", 78 | "profile_desc": "Disse oplysninger vil blive vist offentligt, så vær forsigtig med, hvad du deler.", 79 | "language": "Sprog", 80 | "notifications": "Notifikationer", 81 | "save_and_reload": "Gem og Genindlæs", 82 | "select_a_client": "Vælg en Kunde", 83 | "select_an_engineer": "Vælg en Ingeniør", 84 | "ticket_create": "Opret Billet", 85 | "internallycommented_at": "Kommenteret Internt kl." 86 | } 87 | -------------------------------------------------------------------------------- /apps/client/locales/he/peppermint.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello_good": "טוב", 3 | "hello_morning": "בוקר", 4 | "hello_afternoon": "אחר הצהריים", 5 | "sl_dashboard": "לוח בקרה", 6 | "sl_tickets": "קריאות", 7 | "sl_history": "היסטוריה", 8 | "sl_notebook": "מחברת אישית", 9 | "sl_users": "משתמשים", 10 | "sl_clients": "לקוחות", 11 | "sl_settings": "הגדרות", 12 | "open_tickets": "קריאות פתוחות", 13 | "completed_tickets": "קריאות שנסתיימו", 14 | "account_status": "מצב חשבון", 15 | "todo_list": "רשימת מטלות", 16 | "personal_files": "קבצים אישיים", 17 | "create_ticket": "צור קריאה", 18 | "ticket_new": "קריאה חדשה", 19 | "ticket_name_here": "שם", 20 | "ticket_email_here": "מייל", 21 | "ticket_details": "כותרת", 22 | "ticket_select_client": "בחר לקוח", 23 | "ticket_select_eng": "בחר טכנאי", 24 | "ticket_extra_details": "הזן פרטים נוספים כאן... סימון נתמך", 25 | "cancel": "לְבַטֵל", 26 | "create": "לִיצוֹר", 27 | "low": "נמוך", 28 | "normal": "רגיל", 29 | "high": "גבוה", 30 | "edit-btn": "ערוך", 31 | "reset_password": "אפס סיסמא", 32 | "internal_users": "משתמשים פנימיים", 33 | "clients": "קליאנטים", 34 | "new_user": "משתמש חדש", 35 | "new_client": "לקוח חדש", 36 | "previous": "קודם", 37 | "next": "הבא", 38 | "show": "הצג", 39 | "name": "שם", 40 | "email": "מייל", 41 | "settings": "הגדרות", 42 | "webhooks": "Webhooks", 43 | "version": "גרסא", 44 | "v_profile": "הצג פרופיל", 45 | "reminders": "תזכורות", 46 | "title": "כותרת", 47 | "priority": "תעדוף", 48 | "status": "סטטוס", 49 | "created": "נוצר", 50 | "assigned_to": "משוייך אל", 51 | "enter_todo": "הכנס משימה", 52 | "notebooks": "מחברות", 53 | "notebooks_description": "זהו מחברת אישית. כאן תוכל לשמור הערות, קישורים, קטעי קוד וכו", 54 | "create_notebook": "צור מחברת", 55 | "open": "פתוח", 56 | "assigned_to_me": "משוייכות אלי", 57 | "unassigned": "לא הוקצה", 58 | "closed": "סגור", 59 | "description": "תיאור", 60 | "comments": "הערות", 61 | "leave_comment": "השאר הערה", 62 | "close_issue": "סגור קריאה", 63 | "comment": "הערה", 64 | "save": "שמור", 65 | "labels": "תוויות", 66 | "created_at": "נוצר ב", 67 | "updated_at": "עודכן ב", 68 | "hide_ticket": "הסתר כרטיס", 69 | "show_ticket": "הצג כרטיס גלובלי", 70 | "open_issue": "פתח נושא", 71 | "closed_issue": "נושא סגור", 72 | "recent_tickets": "קריאות אחרונות", 73 | "notebook_title": "כותרת המחברת", 74 | "admin_settings": "הגדרות מנהל מערכת", 75 | "profile": "פרופיל", 76 | "logout": "התנתק", 77 | "unassigned_tickets": "קריאות לא משוייכות", 78 | "profile_desc": "מידע זה יוצג בפומבי אז היזהר במה שאתה משתף.", 79 | "language": "שפה", 80 | "notifications": "התראות", 81 | "save_and_reload": "שמור וטען מחדש", 82 | "select_a_client": "בחר לקוח", 83 | "select_an_engineer": "בחר מהנדס", 84 | "ticket_create": "צור קריאה", 85 | "internallycommented_at": "הערה פנימית ב-", 86 | "reopen_issue": "פתח נושא מחדש", 87 | "needs_support": "צריך תמיכה", 88 | "in_progress": "בתהליך", 89 | "in_review": "בסקירה", 90 | "done": "בוצע", 91 | "select_new_user": "בחר משתמש חדש" 92 | } 93 | -------------------------------------------------------------------------------- /apps/client/locales/no/peppermint.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello_good": "God", 3 | "hello_morning": "Morgen", 4 | "hello_afternoon": "Ettermiddag", 5 | "sl_dashboard": "Instrumentpanel", 6 | "sl_tickets": "Saker", 7 | "sl_history": "Historikk", 8 | "sl_notebook": "Personlig Notatbok", 9 | "sl_users": "Brukere", 10 | "sl_clients": "Klienter", 11 | "sl_settings": "Innstillinger", 12 | "open_tickets": "Åpne Saker", 13 | "completed_tickets": "Fullførte Saker", 14 | "account_status": "Kontostatus", 15 | "todo_list": "Oppgaveliste", 16 | "personal_files": "Personlige Filer", 17 | "create_ticket": "Opprett Saker", 18 | "ticket_new": "Ny Saker", 19 | "ticket_name_here": "Navn", 20 | "ticket_email_here": "E-post", 21 | "ticket_details": "Tittel", 22 | "ticket_select_client": "Velg en Kunde", 23 | "ticket_select_eng": "Velg en Ingeniør", 24 | "ticket_extra_details": "Legg til ekstra detaljer her... støtter markdown", 25 | "cancel": "Avbryt", 26 | "create": "Opprett", 27 | "low": "Lav", 28 | "normal": "Normal", 29 | "high": "Høy", 30 | "edit-btn": "Rediger", 31 | "reset_password": "Nullstill Passord", 32 | "internal_users": "Interne Brukere", 33 | "clients": "Klienter", 34 | "new_user": "Ny Bruker", 35 | "new_client": "Ny Klient", 36 | "previous": "Forrige", 37 | "next": "Neste", 38 | "show": "Vis", 39 | "name": "Navn", 40 | "email": "E-post", 41 | "settings": "Innstillinger", 42 | "webhooks": "Webhooks", 43 | "version": "Versjon", 44 | "v_profile": "Vis Profil", 45 | "reminders": "Påminnelser", 46 | "title": "Tittel", 47 | "priority": "Prioritet", 48 | "status": "Status", 49 | "created": "Opprettet", 50 | "assigned_to": "Tildelt til", 51 | "enter_todo": "Legg til Oppgave", 52 | "notebooks": "Notatbøker", 53 | "notebooks_description": "Dette er en personlig notatbok. Her kan du lagre notater, lenker, kodesnutter, osv.", 54 | "create_notebook": "Opprett Notatbok", 55 | "open": "Åpne", 56 | "assigned_to_me": "Tildelt til Meg", 57 | "unassigned": "Ikke Tildelt", 58 | "closed": "Lukket", 59 | "description": "Beskrivelse", 60 | "comments": "Kommentarer", 61 | "leave_comment": "Legg Igjen en Kommentar", 62 | "close_issue": "Lukk Problem", 63 | "comment": "Kommentar", 64 | "save": "Lagre", 65 | "labels": "Etiketter", 66 | "created_at": "Opprettet den", 67 | "updated_at": "Oppdatert den", 68 | "hide_ticket": "Skjul Sak", 69 | "show_ticket": "Vis Globalt", 70 | "open_issue": "Åpne Problem", 71 | "closed_issue": "Lukket Problem", 72 | "recent_tickets": "Nylige Saker", 73 | "notebook_title": "Notatbok Tittel", 74 | "admin_settings": "Admininnstillinger", 75 | "profile": "Profil", 76 | "logout": "Logg Ut", 77 | "unassigned_tickets": "Ikke Tildelte Saker", 78 | "profile_desc": "Denne informasjonen vil bli vist offentlig, så vær forsiktig med hva du deler.", 79 | "language": "Språk", 80 | "notifications": "Varsler", 81 | "save_and_reload": "Lagre og Last Inn på Nytt", 82 | "select_a_client": "Velg en Kunde", 83 | "select_an_engineer": "Velg en Ingeniør", 84 | "ticket_create": "Opprett Sak", 85 | "internallycommented_at": "Kommentert Internt kl." 86 | } 87 | -------------------------------------------------------------------------------- /apps/client/locales/pt/peppermint.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello_good": "Bom", 3 | "hello_morning": "Dia", 4 | "hello_afternoon": "Tarde", 5 | "sl_dashboard": "Painel", 6 | "sl_tickets": "Tickets", 7 | "sl_history": "Histórico", 8 | "sl_notebook": "Bloco de Notas", 9 | "sl_users": "Usuários", 10 | "sl_clients": "Clientes", 11 | "sl_settings": "Configurações", 12 | "open_tickets": "Tickets Abertos", 13 | "completed_tickets": "Tickets Concluídos", 14 | "account_status": "Status da Conta", 15 | "todo_list": "Lista de Tarefas", 16 | "personal_files": "Arquivos Pessoais", 17 | "create_ticket": "Criar Ticket", 18 | "ticket_new": "Novo Ticket", 19 | "ticket_name_here": "Nome", 20 | "ticket_email_here": "E-mail", 21 | "ticket_details": "Título", 22 | "ticket_select_client": "Selecionar um Cliente", 23 | "ticket_select_eng": "Selecionar um Responsável", 24 | "ticket_extra_details": "Insira detalhes adicionais aqui... suporte para markdown", 25 | "cancel": "Cancelar", 26 | "create": "Criar", 27 | "low": "Baixa", 28 | "normal": "Normal", 29 | "high": "Alta", 30 | "edit-btn": "Editar", 31 | "reset_password": "Redefinir Senha", 32 | "internal_users": "Usuários Internos", 33 | "clients": "Clientes", 34 | "new_user": "Novo Usuário", 35 | "new_client": "Novo Cliente", 36 | "previous": "Anterior", 37 | "next": "Próximo", 38 | "show": "Exibir", 39 | "name": "Nome", 40 | "email": "E-mail", 41 | "settings": "Configurações", 42 | "webhooks": "Webhooks", 43 | "version": "Versão", 44 | "v_profile": "Ver Perfil", 45 | "reminders": "Lembretes", 46 | "title": "Título", 47 | "priority": "Prioridade", 48 | "status": "Status", 49 | "created": "Criado", 50 | "assigned_to": "Atribuído a", 51 | "enter_todo": "Inserir Tarefa", 52 | "notebooks": "Blocos de Notas", 53 | "notebooks_description": "Este é um bloco de notas. Aqui você pode salvar notas, links, trechos de código, etc.", 54 | "create_notebook": "Criar bloco de notas", 55 | "open": "Abrir", 56 | "assigned_to_me": "Atribuído a Mim", 57 | "unassigned": "Não Atribuído", 58 | "closed": "Fechado", 59 | "description": "Descrição", 60 | "comments": "Comentários", 61 | "leave_comment": "Deixar um Comentário", 62 | "close_issue": "Fechar Ticket", 63 | "comment": "Comentário", 64 | "save": "Salvar", 65 | "labels": "Tags", 66 | "created_at": "Criado em", 67 | "updated_at": "Atualizado em", 68 | "hide_ticket": "Ocultar Ticket", 69 | "show_ticket": "Mostrar Global", 70 | "open_issue": "Abrir Ticket", 71 | "closed_issue": "Ticket Fechado", 72 | "recent_tickets": "Tickets Recentes", 73 | "notebook_title": "Título do Bloco de Notas", 74 | "admin_settings": "Configurações de Admin", 75 | "profile": "Perfil", 76 | "logout": "Sair", 77 | "unassigned_tickets": "Tickets não Atribuídos", 78 | "profile_desc": "Estas informações serão exibidas publicamente, então tenha cuidado com o que compartilha.", 79 | "language": "Idioma", 80 | "notifications": "Notificações", 81 | "save_and_reload": "Salvar e Recarregar", 82 | "select_a_client": "Selecionar um Cliente", 83 | "select_an_engineer": "Selecionar um Responsável", 84 | "ticket_create": "Criar Ticket", 85 | "internallycommented_at": "Comentado Internamente às" 86 | } 87 | -------------------------------------------------------------------------------- /apps/client/locales/se/peppermint.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello_good": "God", 3 | "hello_morning": "morgon", 4 | "hello_afternoon": "eftermiddag", 5 | "sl_dashboard": "Instrumentpanel", 6 | "sl_tickets": "Ärenden", 7 | "sl_history": "Historik", 8 | "sl_notebook": "Anteckningsbok", 9 | "sl_users": "Användare", 10 | "sl_clients": "Kunder", 11 | "sl_settings": "Inställningar", 12 | "open_tickets": "Öppna ärenden", 13 | "completed_tickets": "Avslutade ärenden", 14 | "account_status": "Kontostatus", 15 | "todo_list": "Att göra-lista", 16 | "personal_files": "Personliga filer", 17 | "create_ticket": "Skapa ärende", 18 | "ticket_new": "Nytt ärende", 19 | "ticket_name_here": "Namn", 20 | "ticket_email_here": "E-post", 21 | "ticket_details": "Rubrik", 22 | "ticket_select_client": "Välj kund", 23 | "ticket_select_eng": "Välj tekniker", 24 | "ticket_extra_details": "Ange extra detaljer här... markdown stöds", 25 | "cancel": "Avbryt", 26 | "create": "Skapa", 27 | "low": "Låg", 28 | "normal": "Normal", 29 | "high": "Hög", 30 | "edit-btn": "Redigera", 31 | "reset_password": "Återställ lösenord", 32 | "internal_users": "Intern användare", 33 | "clients": "Kunder", 34 | "new_user": "Ny användare", 35 | "new_client": "Ny Kund", 36 | "previous": "Föregående", 37 | "next": "Nästa", 38 | "show": "Visa", 39 | "name": "Namn", 40 | "email": "E-post", 41 | "settings": "Inställningar", 42 | "webhooks": "Webhooks", 43 | "version": "Version", 44 | "v_profile": "Visa profil", 45 | "reminders": "Påminnelser", 46 | "title": "Rubrik", 47 | "priority": "Prioritet", 48 | "status": "Status", 49 | "created": "Skapad", 50 | "assigned_to": "Tilldelad till", 51 | "enter_todo": "Ange att-göra", 52 | "notebooks": "Anteckningsböcker", 53 | "notebooks_description": "Detta är en personlig anteckningsbok. Här kan du spara anteckningar, länkar, kodsnuttar, osv.", 54 | "create_notebook": "Skapa anteckningsbok", 55 | "open": "Öppna", 56 | "assigned_to_me": "Tilldelad till mig", 57 | "unassigned": "Ej tilldelad", 58 | "closed": "Stängd", 59 | "description": "Beskrivning", 60 | "comments": "Kommentarer", 61 | "leave_comment": "Lämna en kommentar", 62 | "close_issue": "Stäng ärende", 63 | "comment": "Kommentar", 64 | "save": "Spara", 65 | "labels": "Etiketter", 66 | "created_at": "Skapad den", 67 | "updated_at": "Uppdaterad den", 68 | "hide_ticket": "Dölj ärende", 69 | "show_ticket": "Visa ärende", 70 | "open_issue": "Öppna ärenden", 71 | "closed_issue": "Stängda ärenden", 72 | "recent_tickets": "Senaste ärenden", 73 | "notebook_title": "Anteckningsbokstitel", 74 | "admin_settings": "Admininställningar", 75 | "profile": "Profil", 76 | "logout": "Logga ut", 77 | "unassigned_tickets": "Ej tilldelade ärenden", 78 | "profile_desc": "Denna information kommer att visas offentligt, så var försiktig med vad du delar.", 79 | "language": "Språk", 80 | "notifications": "Notifieringar", 81 | "save_and_reload": "Spara och ladda Om", 82 | "select_a_client": "Välj en kund", 83 | "select_an_engineer": "Välj en tekniker", 84 | "ticket_create": "Skapa ett ärende", 85 | "internallycommented_at": "Kommenterat Internt kl." 86 | } 87 | -------------------------------------------------------------------------------- /apps/client/locales/zh-CN/peppermint.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello_good": "", 3 | "hello_morning": "早上好", 4 | "hello_afternoon": "下午好", 5 | "sl_dashboard": "仪表盘", 6 | "sl_tickets": "工单", 7 | "sl_history": "历史记录", 8 | "sl_notebook": "个人笔记本", 9 | "sl_users": "用户", 10 | "sl_clients": "客户", 11 | "sl_settings": "设置", 12 | "open_tickets": "打开的工单", 13 | "completed_tickets": "已完成的工单", 14 | "account_status": "账户状态", 15 | "todo_list": "待办事项", 16 | "personal_files": "个人文件", 17 | "create_ticket": "创建工单", 18 | "ticket_new": "新工单", 19 | "ticket_name_here": "姓名", 20 | "ticket_email_here": "电子邮件", 21 | "ticket_details": "标题", 22 | "ticket_select_client": "选择客户", 23 | "ticket_select_eng": "选择工程师", 24 | "ticket_extra_details": "在此输入额外的详细信息... 支持Markdown", 25 | "cancel": "取消", 26 | "create": "创建", 27 | "low": "低", 28 | "normal": "正常", 29 | "high": "高", 30 | "edit-btn": "编辑", 31 | "reset_password": "重置密码", 32 | "internal_users": "内部用户", 33 | "clients": "客户", 34 | "new_user": "新用户", 35 | "new_client": "新客户", 36 | "previous": "上一页", 37 | "next": "下一页", 38 | "show": "显示", 39 | "name": "姓名", 40 | "email": "电子邮件", 41 | "settings": "设置", 42 | "webhooks": "Webhooks", 43 | "version": "版本", 44 | "v_profile": "查看个人资料", 45 | "reminders": "提醒", 46 | "title": "标题", 47 | "priority": "优先级", 48 | "status": "状态", 49 | "created": "创建时间", 50 | "assigned_to": "分配给", 51 | "enter_todo": "输入待办事项", 52 | "notebooks": "笔记本", 53 | "notebooks_description": "这是一个个人笔记本。您可以在这里保存笔记、链接、代码片段等。", 54 | "create_notebook": "创建笔记本", 55 | "open": "打开", 56 | "assigned_to_me": "分配给我的", 57 | "unassigned": "未分配的", 58 | "closed": "已关闭", 59 | "description": "描述", 60 | "comments": "评论", 61 | "leave_comment": "留下评论", 62 | "close_issue": "关闭问题", 63 | "comment": "评论", 64 | "save": "保存", 65 | "labels": "标签", 66 | "created_at": "创建于", 67 | "updated_at": "更新于", 68 | "hide_ticket": "隐藏工单", 69 | "show_ticket": "显示全局", 70 | "open_issue": "打开问题", 71 | "closed_issue": "关闭问题", 72 | "recent_tickets": "最近的工单", 73 | "notebook_title": "笔记本标题", 74 | "admin_settings": "管理员设置", 75 | "profile": "个人资料", 76 | "logout": "登出", 77 | "unassigned_tickets": "未分配的工单", 78 | "profile_desc": "此信息将公开显示,请谨慎分享。", 79 | "language": "语言", 80 | "notifications": "通知", 81 | "save_and_reload": "保存并重新加载", 82 | "select_a_client": "选择客户", 83 | "select_an_engineer": "选择工程师", 84 | "ticket_create": "创建工单", 85 | "internallycommented_at": "内部评论于", 86 | "reopen_issue": "重新打开问题", 87 | "needs_support": "需要支持", 88 | "in_progress": "进行中", 89 | "in_review": "审核中", 90 | "done": "已完成", 91 | "select_new_user": "选择新用户" 92 | } 93 | -------------------------------------------------------------------------------- /apps/client/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/client/next.config.js: -------------------------------------------------------------------------------- 1 | // next.config.js 2 | const withPlugins = require('next-compose-plugins'); 3 | const removeImports = require('next-remove-imports')(); 4 | const nextTranslate = require('next-translate'); 5 | const withPWA = require('next-pwa')({ 6 | dest: 'public', 7 | register: true, 8 | skipWaiting: true, 9 | disable: false, 10 | }); 11 | 12 | module.exports = withPlugins( 13 | [removeImports, nextTranslate, withPWA], 14 | { 15 | reactStrictMode: false, 16 | swcMinify: true, 17 | output: 'standalone', 18 | 19 | async rewrites() { 20 | return [ 21 | { 22 | source: '/api/v1/:path*', 23 | destination: 'http://localhost:5003/api/v1/:path*', 24 | }, 25 | ]; 26 | }, 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /apps/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@atlaskit/pragmatic-drag-and-drop": "^1.5.0", 12 | "@blocknote/core": "^0.17.1", 13 | "@blocknote/mantine": "^0.17.1", 14 | "@blocknote/react": "^0.17.1", 15 | "@headlessui/react": "^1.4.2", 16 | "@heroicons/react": "^2.0.18", 17 | "@monaco-editor/react": "^4.6.0", 18 | "@radix-ui/react-alert-dialog": "^1.1.2", 19 | "@radix-ui/react-avatar": "^1.1.1", 20 | "@radix-ui/react-collapsible": "^1.1.1", 21 | "@radix-ui/react-context-menu": "^2.2.2", 22 | "@radix-ui/react-dialog": "^1.1.2", 23 | "@radix-ui/react-dropdown-menu": "^2.1.2", 24 | "@radix-ui/react-label": "^2.1.0", 25 | "@radix-ui/react-popover": "^1.0.7", 26 | "@radix-ui/react-select": "^2.0.0", 27 | "@radix-ui/react-separator": "^1.1.0", 28 | "@radix-ui/react-slot": "^1.1.0", 29 | "@radix-ui/react-switch": "^1.1.1", 30 | "@radix-ui/react-toast": "^1.2.2", 31 | "@radix-ui/react-tooltip": "^1.1.3", 32 | "@radix-ui/themes": "^2.0.1", 33 | "@tabler/icons-react": "^3.5.0", 34 | "@tailwindcss/forms": "^0.4.0", 35 | "@tailwindcss/typography": "^0.5.13", 36 | "add": "^2.0.6", 37 | "avatar": "^0.1.0", 38 | "axios": "^0.25.0", 39 | "class-variance-authority": "^0.7.0", 40 | "clsx": "^2.1.1", 41 | "cmdk": "1.0.2", 42 | "cookies-next": "^3.0.0", 43 | "js-file-download": "^0.4.12", 44 | "lodash": "^4.17.21", 45 | "lucide-react": "^0.453.0", 46 | "moment": "^2.29.1", 47 | "next": "13.5", 48 | "next-compose-plugins": "^2.2.1", 49 | "next-pwa": "^5.6.0", 50 | "next-remove-imports": "^1.0.6", 51 | "next-themes": "^0.3.0", 52 | "next-translate": "^1.3.4", 53 | "npx": "^10.2.2", 54 | "posthog-js": "1.93.2", 55 | "prismjs": "^1.29.0", 56 | "prop-types": "^15.8.1", 57 | "react": "^18.3.1", 58 | "react-dom": "^18.3.1", 59 | "react-frame-component": "^5.2.6", 60 | "react-query": "^3.34.7", 61 | "react-resizable-panels": "^2.0.19", 62 | "react-simple-code-editor": "^0.13.1", 63 | "react-spinners": "^0.11.0", 64 | "react-table": "^7.7.0", 65 | "shadcn": "^2.1.6", 66 | "tailwind-merge": "^2.3.0", 67 | "tailwind-scrollbar-hide": "^2.0.0", 68 | "tailwindcss-animate": "^1.0.7", 69 | "use-debounce": "^10.0.1" 70 | }, 71 | "devDependencies": { 72 | "@types/add": "^2", 73 | "@types/lodash": "^4", 74 | "@types/next": "^9.0.0", 75 | "@types/next-pwa": "^5", 76 | "@types/node": "17.0.4", 77 | "@types/prismjs": "^1", 78 | "@types/prop-types": "^15", 79 | "@types/react": "18.2.38", 80 | "@types/react-table": "^7.7.15", 81 | "autoprefixer": "^10.4.0", 82 | "postcss": "^8.4.5", 83 | "tailwindcss": "^3.0.7", 84 | "terser-webpack-plugin": "^5.3.3", 85 | "typescript": "5.4" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /apps/client/pages/404.tsx: -------------------------------------------------------------------------------- 1 | export default function customError() { 2 | return ( 3 |
4 | 5 |

6 | Hmmm there seems to be an error, this page does not exist. 7 |

8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/client/pages/_document.js: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | Peppermint 23 | 24 | 25 | 31 | 37 | 38 | 39 | 40 |
41 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /apps/client/pages/admin/email-queues/oauth.tsx: -------------------------------------------------------------------------------- 1 | import { getCookie, setCookie } from "cookies-next"; 2 | import { useRouter } from "next/router"; 3 | import { useEffect } from "react"; 4 | 5 | export default function Login() { 6 | const router = useRouter(); 7 | 8 | async function check() { 9 | if (router.query.code) { 10 | await fetch( 11 | `/api/v1/email-queue/oauth/gmail?code=${router.query.code}&mailboxId=${router.query.state}`, 12 | { 13 | headers: { 14 | "Content-Type": "application/json", 15 | Authorization: `Bearer ${getCookie("session")}`, 16 | }, 17 | } 18 | ) 19 | .then((res) => res.json()) 20 | .then((res) => { 21 | if (res.success) { 22 | router.push("/admin/email-queues"); 23 | } 24 | }); 25 | } 26 | } 27 | 28 | useEffect(() => { 29 | check(); 30 | }, [router]); 31 | 32 | return
; 33 | } 34 | -------------------------------------------------------------------------------- /apps/client/pages/admin/index.js: -------------------------------------------------------------------------------- 1 | export default function BlankPage() { 2 | return ( 3 |
4 |
5 | logo 6 |

7 | Peppermint 8 |

9 |
10 |

11 | Welcome to peppermint! Thank you for checking us out! 12 |

13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/client/pages/admin/logs.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardHeader, CardTitle } from "@/shadcn/ui/card"; 2 | import { getCookie } from "cookies-next"; 3 | import { useEffect, useState } from "react"; 4 | 5 | const Logs = () => { 6 | const [logs, setLogs] = useState([]); 7 | const [loading, setLoading] = useState(true); 8 | 9 | const fetchLogs = async () => { 10 | const response = await fetch("/api/v1/data/logs", { 11 | headers: { 12 | Authorization: `Bearer ${getCookie("session")}`, 13 | }, 14 | }); 15 | const data = await response.json(); 16 | 17 | // Split logs by newline and parse each line as JSON 18 | const parsedLogs = data.logs 19 | .split("\n") 20 | .filter((line) => line.trim()) // Remove empty lines 21 | .map((line) => { 22 | try { 23 | return JSON.parse(line); 24 | } catch (e) { 25 | console.error("Failed to parse log line:", e); 26 | return null; 27 | } 28 | }) 29 | .filter((log) => log !== null) // Remove any 30 | .sort((a, b) => b.time - a.time); 31 | 32 | setLogs(parsedLogs); 33 | setLoading(false); 34 | }; 35 | 36 | useEffect(() => { 37 | fetchLogs(); 38 | }, []); 39 | 40 | if (loading) { 41 | return
Loading...
; 42 | } 43 | 44 | return ( 45 |
46 | 55 | 56 | 57 | Logs 58 | 59 | 60 | {logs.length === 0 ? ( 61 |
No logs available
62 | ) : ( 63 |
    64 | {logs.map((log, index) => ( 65 |
  • 66 |
    67 | 68 | {new Date(log.time).toLocaleString()} 69 | 70 | {log.msg} 71 |
    72 |
  • 73 | ))} 74 |
75 | )} 76 |
77 |
78 |
79 | ); 80 | }; 81 | 82 | export default Logs; 83 | -------------------------------------------------------------------------------- /apps/client/pages/admin/smtp/oauth.tsx: -------------------------------------------------------------------------------- 1 | import { getCookie, setCookie } from "cookies-next"; 2 | import { useRouter } from "next/router"; 3 | import { useEffect } from "react"; 4 | 5 | export default function Login() { 6 | const router = useRouter(); 7 | 8 | async function check() { 9 | if (router.query.code) { 10 | await fetch( 11 | `/api/v1/config/email/oauth/gmail?code=${router.query.code}`, 12 | { 13 | headers: { 14 | "Content-Type": "application/json", 15 | Authorization: `Bearer ${getCookie("session")}`, 16 | }, 17 | } 18 | ) 19 | .then((res) => res.json()) 20 | .then((res) => { 21 | if (res.success) { 22 | router.push("/admin/smtp"); 23 | } 24 | }); 25 | } 26 | } 27 | 28 | useEffect(() => { 29 | check(); 30 | }, [router]); 31 | 32 | return
; 33 | } 34 | -------------------------------------------------------------------------------- /apps/client/pages/admin/smtp/templates/[id].tsx: -------------------------------------------------------------------------------- 1 | import { toast } from "@/shadcn/hooks/use-toast"; 2 | import { getCookie } from "cookies-next"; 3 | import { useRouter } from "next/router"; 4 | import { highlight, languages } from "prismjs"; 5 | import "prismjs/components/prism-clike"; 6 | import "prismjs/themes/prism.css"; 7 | import { useEffect, useState } from "react"; 8 | import Editor from "react-simple-code-editor"; 9 | 10 | export default function EmailTemplates() { 11 | const [template, setTemplate] = useState(); 12 | 13 | const router = useRouter(); 14 | 15 | const [code, setCode] = useState(`function add(a, b) {\n return a + b;\n}`); 16 | 17 | async function fetchTemplate() { 18 | await fetch(`/api/v1/ticket/template/${router.query.id}`, { 19 | method: "GET", 20 | headers: { 21 | "Content-Type": "application/json", 22 | Authorization: `Bearer ${getCookie("session")}`, 23 | }, 24 | }) 25 | .then((response) => response.json()) 26 | .then((data) => { 27 | if (data.success) { 28 | console.log(data); 29 | setTemplate(data.template[0].html); 30 | } 31 | }); 32 | } 33 | 34 | async function updateTemplate() { 35 | await fetch(`/api/v1/ticket/template/${router.query.id}`, { 36 | method: "PUT", 37 | headers: { 38 | "Content-Type": "application/json", 39 | Authorization: `Bearer ${getCookie("session")}`, 40 | }, 41 | body: JSON.stringify({ html: template }), 42 | }) 43 | .then((response) => response.json()) 44 | .then((data) => { 45 | if (data.success) { 46 | toast({ 47 | variant: "default", 48 | title: "Success", 49 | description: `Template updated`, 50 | }); 51 | } 52 | }); 53 | } 54 | 55 | useEffect(() => { 56 | fetchTemplate(); 57 | }, []); 58 | 59 | return ( 60 |
61 |
62 | 69 |
70 |
71 |
72 | {template !== undefined && ( 73 | setTemplate(code)} 76 | highlight={(code) => highlight(code, languages.js, "html")} 77 | padding={10} 78 | style={{ 79 | fontFamily: '"Fira code", "Fira Mono", monospace', 80 | fontSize: 12, 81 | overflow: "scroll", 82 | }} 83 | textareaClassName="overflow-scroll" 84 | /> 85 | )} 86 |
87 |
88 | 89 |
90 | 91 |
92 |
93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /apps/client/pages/auth/oauth.tsx: -------------------------------------------------------------------------------- 1 | import { setCookie } from "cookies-next"; 2 | import { useRouter } from "next/router"; 3 | import { useEffect } from "react"; 4 | 5 | export default function Login() { 6 | const router = useRouter(); 7 | 8 | async function check() { 9 | if (router.query.code) { 10 | const sso = await fetch( 11 | `/api/v1/auth/oauth/callback?code=${router.query.code}` 12 | ).then((res) => res.json()); 13 | 14 | if (!sso.success) { 15 | router.push("/auth/login?error=account_not_found"); 16 | } else { 17 | setandRedirect(sso.token); 18 | } 19 | } 20 | } 21 | 22 | function setandRedirect(token) { 23 | setCookie("session", token, { maxAge: 60 * 6 * 24 }); 24 | router.push("/onboarding"); 25 | } 26 | 27 | useEffect(() => { 28 | check(); 29 | }, [router]); 30 | 31 | return
; 32 | } 33 | -------------------------------------------------------------------------------- /apps/client/pages/auth/oidc.tsx: -------------------------------------------------------------------------------- 1 | import { setCookie } from "cookies-next"; 2 | import { useRouter } from "next/router"; 3 | import { useEffect } from "react"; 4 | 5 | export default function Login() { 6 | const router = useRouter(); 7 | 8 | async function check() { 9 | if (router.query.code) { 10 | const sso = await fetch( 11 | `/api/v1/auth/oidc/callback?state=${router.query.state}&code=${router.query.code}&session_state=${router.query.session_state}&iss=${router.query.iss}` 12 | ).then((res) => res.json()); 13 | 14 | if (!sso.success) { 15 | router.push("/auth/login?error=account_not_found"); 16 | } else { 17 | setandRedirect(sso.token, sso.onboarding); 18 | } 19 | } 20 | } 21 | 22 | function setandRedirect(token: string, onboarding: boolean) { 23 | setCookie("session", token, { maxAge: 60 * 6 * 24 }); 24 | router.push(onboarding ? "/onboarding" : "/"); 25 | } 26 | 27 | useEffect(() => { 28 | check(); 29 | }, [router]); 30 | 31 | return
; 32 | } 33 | -------------------------------------------------------------------------------- /apps/client/pages/documents/[id].tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | 3 | import "@blocknote/core/fonts/inter.css"; 4 | import "@blocknote/mantine/style.css"; 5 | 6 | const Editor = dynamic(() => import("../../components/NotebookEditor"), { 7 | ssr: false, 8 | }); 9 | 10 | export default function Notebooks() { 11 | 12 | return ( 13 | <> 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/client/pages/issue/[id].tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | 3 | import "@blocknote/core/fonts/inter.css"; 4 | import "@blocknote/mantine/style.css"; 5 | 6 | const Editor = dynamic(() => import("../../components/TicketDetails"), { ssr: false }); 7 | 8 | export default function TicketByID() { 9 | return ( 10 |
11 | 12 |
13 | ); 14 | } -------------------------------------------------------------------------------- /apps/client/pages/notifications.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { getCookie } from "cookies-next"; 3 | import moment from "moment"; 4 | import Link from "next/link"; 5 | import { useUser } from "../store/session"; 6 | 7 | export default function Tickets() { 8 | 9 | const token = getCookie("session"); 10 | 11 | const { user, fetchUserProfile } = useUser(); 12 | 13 | async function markasread(id) { 14 | await fetch(`/api/v1/user/notifcation/${id}`, { 15 | method: "GET", 16 | headers: { 17 | Authorization: `Bearer ${token}`, 18 | }, 19 | }).then((res) => res.json()); 20 | await fetchUserProfile(); 21 | } 22 | 23 | 24 | return ( 25 |
26 |
27 |
28 | 29 | You have {user.notifcations.filter((e) => !e.read).length} unread 30 | notifcations 31 | {user.notifcations.length > 1 ? "'s" : ""} 32 | 33 |
34 | {user.notifcations.filter((e) => !e.read).length > 0 ? ( 35 | user.notifcations 36 | .filter((e) => !e.read) 37 | .map((item) => { 38 | return ( 39 | 40 |
41 |
42 | {item.text} 43 |
44 |
45 | 55 | 56 | {moment(item.createdAt).format("DD/MM/yyyy")} 57 | 58 |
59 |
60 | 61 | ); 62 | }) 63 | ) : ( 64 |
65 | 66 | You have no notifcations 67 | 68 |
69 | )} 70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /apps/client/pages/settings/flags.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import { useEffect, useState } from "react"; 3 | 4 | interface FeatureFlag { 5 | name: string; 6 | enabled: boolean; 7 | description: string; 8 | flagKey: string; 9 | } 10 | 11 | const defaultFlags: FeatureFlag[] = [ 12 | { 13 | name: "Hide Keyboard Shortcuts", 14 | enabled: false, 15 | description: "Hide keyboard shortcuts", 16 | flagKey: "keyboard_shortcuts_hide", // Added flag key for this feature 17 | }, 18 | { 19 | name: "Hide Name in Create", 20 | enabled: false, 21 | description: "Hide name field in create a new issue", 22 | flagKey: "name_hide", // Added flag key for this feature 23 | }, 24 | { 25 | name: "Hide Email in Create", 26 | enabled: false, 27 | description: "Hide email field in create a new issue", 28 | flagKey: "email_hide", // Added flag key for this feature 29 | }, 30 | ]; 31 | 32 | export default function FeatureFlags() { 33 | const [flags, setFlags] = useState([]); 34 | const router = useRouter(); 35 | 36 | useEffect(() => { 37 | // Load flags from localStorage on component mount 38 | const savedFlags = localStorage.getItem("featureFlags"); 39 | if (savedFlags) { 40 | const parsedFlags = JSON.parse(savedFlags); 41 | // Merge saved flags with default flags, adding any new flags 42 | const mergedFlags = defaultFlags.map(defaultFlag => { 43 | const savedFlag = parsedFlags.find((f: FeatureFlag) => f.name === defaultFlag.name); 44 | return savedFlag || defaultFlag; 45 | }); 46 | setFlags(mergedFlags); 47 | localStorage.setItem("featureFlags", JSON.stringify(mergedFlags)); 48 | } else { 49 | setFlags(defaultFlags); 50 | localStorage.setItem("featureFlags", JSON.stringify(defaultFlags)); 51 | } 52 | }, []); 53 | 54 | const toggleFlag = (flagName: string) => { 55 | const updatedFlags = flags.map((flag) => 56 | flag.name === flagName ? { ...flag, enabled: !flag.enabled } : flag 57 | ); 58 | setFlags(updatedFlags); 59 | localStorage.setItem("featureFlags", JSON.stringify(updatedFlags)); 60 | router.reload(); 61 | }; 62 | 63 | return ( 64 |
65 |

Feature Flags

66 |
67 | {flags.map((flag) => ( 68 |
72 |
73 |
{flag.name}
74 |
{flag.description}
75 |
76 |
77 | 80 |
81 |
82 | ))} 83 |
84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /apps/client/pages/settings/index.tsx: -------------------------------------------------------------------------------- 1 | export default function PasswordChange() { 2 | return ( 3 |
4 |
5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /apps/client/pages/settings/password.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import { getCookie } from "cookies-next"; 4 | import { toast } from "@/shadcn/hooks/use-toast"; 5 | 6 | export default function PasswordChange({ children }) { 7 | const token = getCookie("session"); 8 | 9 | const [password, setPassword] = useState(""); 10 | const [check, setCheck] = useState(""); 11 | 12 | const postData = async () => { 13 | if (check === password && password.length > 2) { 14 | await fetch(`/api/v1/auth/reset-password`, { 15 | method: "POST", 16 | headers: { 17 | "Content-Type": "application/json", 18 | Authorization: "Bearer " + token, 19 | }, 20 | body: JSON.stringify({ 21 | password, 22 | }), 23 | }) 24 | .then((res) => res.json()) 25 | .then((res) => { 26 | if (res.success) { 27 | toast({ 28 | variant: "default", 29 | title: "Success", 30 | description: "Password updated successfully.", 31 | }); 32 | } else { 33 | toast({ 34 | variant: "destructive", 35 | title: "Error", 36 | description: "Error: Failed to update password", 37 | }); 38 | } 39 | }); 40 | } else { 41 | toast({ 42 | variant: "destructive", 43 | title: "Error", 44 | description: "Error: passwords do not match", 45 | }); 46 | } 47 | }; 48 | 49 | return ( 50 | <> 51 |
52 |
53 |
54 | setPassword(e.target.value)} 58 | placeholder="Enter users new password" 59 | /> 60 | 61 | setCheck(e.target.value)} 65 | placeholder="Confirm users password" 66 | /> 67 |
68 |
69 |
70 | 79 |
80 |
81 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /apps/client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /apps/client/public/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /apps/client/public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/apps/client/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /apps/client/public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/apps/client/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /apps/client/public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/apps/client/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/client/public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/apps/client/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /apps/client/public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/apps/client/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /apps/client/public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/apps/client/public/favicon/favicon.ico -------------------------------------------------------------------------------- /apps/client/public/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /apps/client/public/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Peppermint", 3 | "short_name": "Peppermint", 4 | "start_url": "/", 5 | "display": "standalone", 6 | "background_color": "#ffffff", 7 | "description": "An offline-first Next.js app", 8 | "icons": [ 9 | { 10 | "src": "./favicon/android-chrome-192x192.png", 11 | "type": "image/png", 12 | "sizes": "192x192" 13 | }, 14 | { 15 | "src": "./favicon/android-chrome-512x512.png", 16 | "type": "image/png", 17 | "sizes": "512x512" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /apps/client/store/session.js: -------------------------------------------------------------------------------- 1 | // UserContext.js 2 | import { getCookie } from "cookies-next"; 3 | import { useRouter } from "next/router"; 4 | import posthog from "posthog-js"; 5 | import { PostHogProvider } from "posthog-js/react"; 6 | import { createContext, useContext, useEffect, useState } from "react"; 7 | 8 | const UserContext = createContext(); 9 | 10 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG); 11 | 12 | export const SessionProvider = ({ children }) => { 13 | const router = useRouter(); 14 | const [user, setUser] = useState(null); 15 | const [loading, setLoading] = useState(true); 16 | 17 | const fetchUserProfile = async () => { 18 | const token = getCookie("session"); 19 | try { 20 | await fetch(`/api/v1/auth/profile`, { 21 | method: "GET", 22 | headers: { 23 | "Content-Type": "application/json", 24 | Authorization: `Bearer ${token}`, 25 | }, 26 | }) 27 | .then((res) => res.json()) 28 | .then((res) => { 29 | if (res.user) { 30 | setUser(res.user); 31 | setLoading(false); 32 | } else { 33 | console.error("Failed to fetch user profile"); 34 | router.push("/auth/login"); 35 | } 36 | }); 37 | } catch (error) { 38 | // Handle fetch errors if necessary 39 | console.error("Error fetching user profile:", error); 40 | router.push("/auth/login"); 41 | } finally { 42 | setLoading(false); 43 | } 44 | }; 45 | 46 | useEffect(() => { 47 | fetchUserProfile(); 48 | }, [router]); 49 | 50 | return process.env.NEXT_PUBLIC_ENVIRONMENT === "production" && 51 | process.env.NEXT_PUBLIC_TELEMETRY === "1" ? ( 52 | 53 | {children} 54 | 55 | ) : ( 56 | 57 | {children} 58 | 59 | ); 60 | }; 61 | 62 | export const useUser = () => { 63 | const context = useContext(UserContext); 64 | if (!context) { 65 | throw new Error("useUser must be used within a UserProvider"); 66 | } 67 | return context; 68 | }; 69 | -------------------------------------------------------------------------------- /apps/client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | "./pages/**/*.{ts,tsx}", 6 | "./@/shadcn/**/*.{ts,tsx}", 7 | "./app/**/*.{ts,tsx}", 8 | "./src/**/*.{ts,tsx}", 9 | "./pages/**/*.{js,ts,jsx,tsx}", 10 | "./components/**/*.{js,ts,jsx,tsx}", 11 | "./layouts/**/*.{js,ts,jsx,tsx}", 12 | ], 13 | prefix: "", 14 | theme: { 15 | container: { 16 | center: 'true', 17 | padding: '2rem', 18 | screens: { 19 | '2xl': '1400px' 20 | } 21 | }, 22 | extend: { 23 | colors: { 24 | border: 'hsl(var(--border))', 25 | input: 'hsl(var(--input))', 26 | ring: 'hsl(var(--ring))', 27 | background: 'hsl(var(--background))', 28 | foreground: 'hsl(var(--foreground))', 29 | primary: { 30 | DEFAULT: 'hsl(var(--primary))', 31 | foreground: 'hsl(var(--primary-foreground))' 32 | }, 33 | secondary: { 34 | DEFAULT: 'hsl(var(--secondary))', 35 | foreground: 'hsl(var(--secondary-foreground))' 36 | }, 37 | destructive: { 38 | DEFAULT: 'hsl(var(--destructive))', 39 | foreground: 'hsl(var(--destructive-foreground))' 40 | }, 41 | muted: { 42 | DEFAULT: 'hsl(var(--muted))', 43 | foreground: 'hsl(var(--muted-foreground))' 44 | }, 45 | accent: { 46 | DEFAULT: 'hsl(var(--accent))', 47 | foreground: 'hsl(var(--accent-foreground))' 48 | }, 49 | popover: { 50 | DEFAULT: 'hsl(var(--popover))', 51 | foreground: 'hsl(var(--popover-foreground))' 52 | }, 53 | card: { 54 | DEFAULT: 'hsl(var(--card))', 55 | foreground: 'hsl(var(--card-foreground))' 56 | }, 57 | sidebar: { 58 | DEFAULT: 'hsl(var(--sidebar-background))', 59 | foreground: 'hsl(var(--sidebar-foreground))', 60 | primary: 'hsl(var(--sidebar-primary))', 61 | 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', 62 | accent: 'hsl(var(--sidebar-accent))', 63 | 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', 64 | border: 'hsl(var(--sidebar-border))', 65 | ring: 'hsl(var(--sidebar-ring))' 66 | } 67 | }, 68 | borderRadius: { 69 | lg: 'var(--radius)', 70 | md: 'calc(var(--radius) - 2px)', 71 | sm: 'calc(var(--radius) - 4px)' 72 | }, 73 | keyframes: { 74 | 'accordion-down': { 75 | from: { 76 | height: '0' 77 | }, 78 | to: { 79 | height: 'var(--radix-accordion-content-height)' 80 | } 81 | }, 82 | 'accordion-up': { 83 | from: { 84 | height: 'var(--radix-accordion-content-height)' 85 | }, 86 | to: { 87 | height: '0' 88 | } 89 | } 90 | }, 91 | animation: { 92 | 'accordion-down': 'accordion-down 0.2s ease-out', 93 | 'accordion-up': 'accordion-up 0.2s ease-out' 94 | } 95 | } 96 | }, 97 | plugins: [ 98 | require("tailwindcss-animate"), 99 | require("@tailwindcss/forms"), 100 | require("@tailwindcss/typography"), 101 | ], 102 | }; 103 | -------------------------------------------------------------------------------- /apps/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "incremental": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "paths": { 21 | "@/*": ["./@/*"] 22 | } 23 | }, 24 | "include": [ 25 | "next-env.d.ts", 26 | "**/*.ts", 27 | "**/*.tsx" 28 | , "pages/_document.js" ], 29 | "exclude": [ 30 | "node_modules" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /apps/docs/next.config.mjs: -------------------------------------------------------------------------------- 1 | import nextra from "nextra"; 2 | 3 | const withNextra = nextra({ 4 | theme: "nextra-theme-docs", 5 | themeConfig: "./theme.config.jsx", 6 | // defaultShowCopyCode: true, 7 | // flexsearch: { 8 | // codeblocks: true, 9 | // }, 10 | // codeHighlight: true, 11 | }); 12 | 13 | export default withNextra(); 14 | 15 | // If you have other Next.js configurations, you can pass them as the parameter: 16 | // export default withNextra({ /* other next.js config */ }) 17 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "dev": "next", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "description": "", 14 | "dependencies": { 15 | "next": "^14.2.15", 16 | "nextra": "^3.0.15", 17 | "nextra-theme-docs": "^3.0.15", 18 | "react": "^18.3.1", 19 | "react-dom": "^18.3.1", 20 | "react-spinners": "^0.13.8" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "18.2.38", 24 | "@types/react-dom": "^18" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/docs/pages/_app.jsx: -------------------------------------------------------------------------------- 1 | export default function App({ Component, pageProps }) { 2 | return 3 | } 4 | -------------------------------------------------------------------------------- /apps/docs/pages/_meta.js: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | title: "Welcome", 4 | }, 5 | "-- getting-started": { 6 | type: "separator", 7 | title: "Getting Started", 8 | }, 9 | docker: "Install with Docker", 10 | "--- Guides": { 11 | type: "separator", 12 | title: "Guides", 13 | }, 14 | installer: "Easy Installer with Spearmint", 15 | "--- Guides": { 16 | type: "separator", 17 | title: "Guides", 18 | }, 19 | proxy: "Proxy", 20 | "--- Configuration": { 21 | type: "separator", 22 | title: "Configuration", 23 | }, 24 | oidc: "OpenID Connect (OIDC)", 25 | "--- Development": { 26 | type: "separator", 27 | title: "Development", 28 | }, 29 | development: "Project Setup Guide", 30 | translation: "Adding a New Language to the Project", 31 | // "--- Reference": { 32 | // type: "separator", 33 | // title: "Reference", 34 | // }, 35 | }; 36 | -------------------------------------------------------------------------------- /apps/docs/pages/development.md: -------------------------------------------------------------------------------- 1 | # Project Setup Guide 2 | 3 | Welcome to the project! This guide will help you set up the project on your local machine for development purposes. 4 | 5 | ## Prerequisites 6 | 7 | Before you begin, ensure you have met the following requirements: 8 | 9 | - **Operating System**: Windows, macOS, or Linux 10 | - **Node.js**: Version 14.x or higher 11 | - **npm**: Version 6.x or higher 12 | - **Git**: Version control system 13 | - **Database**: (e.g., PostgreSQL, MySQL) - Ensure it's installed and running 14 | 15 | ## Installation 16 | 17 | Follow these steps to set up the project: 18 | 19 | 1. **Clone the Repository** 20 | 21 | Open your terminal and run the following command to clone the project repository: 22 | 23 | ```bash 24 | git clone https://github.com/your-username/your-project.git 25 | ``` 26 | 27 | 2. **Navigate to the Project Directory** 28 | 29 | Change into the project directory: 30 | 31 | ```bash 32 | cd your-project 33 | ``` 34 | 35 | 3. **Install Dependencies** 36 | 37 | Run the following command to install the necessary dependencies: 38 | 39 | ```bash 40 | npm install 41 | ``` 42 | 43 | 4. **Set Up Environment Variables** 44 | 45 | Create a `.env` file in the root directory and add the necessary environment variables. You can use the `.env.example` file as a reference. 46 | 47 | ```plaintext 48 | DATABASE_URL=your-database-url 49 | API_KEY=your-api-key 50 | ``` 51 | 52 | 5. **Database Setup** 53 | 54 | If your project requires a database, run the following command to set up the database: 55 | 56 | ```bash 57 | npm run db:setup 58 | ``` 59 | 60 | 6. **Start the Development Server** 61 | 62 | Start the development server using the following command: 63 | 64 | ```bash 65 | npm start 66 | ``` 67 | 68 | The application should now be running on `http://localhost:3000`. 69 | 70 | ## Running Tests 71 | 72 | To run tests, use the following command: 73 | 74 | -------------------------------------------------------------------------------- /apps/docs/pages/docker.md: -------------------------------------------------------------------------------- 1 | # Docker Install 2 | 3 | Requirements: 4 | 5 | - Docker 6 | - Docker Compose 7 | 8 | ```docker 9 | version: "3.1" 10 | 11 | services: 12 | peppermint_postgres: 13 | container_name: peppermint_postgres 14 | image: postgres:latest 15 | restart: always 16 | volumes: 17 | - pgdata:/var/lib/postgresql/data 18 | environment: 19 | POSTGRES_USER: peppermint 20 | POSTGRES_PASSWORD: 1234 21 | POSTGRES_DB: peppermint 22 | 23 | peppermint: 24 | container_name: peppermint 25 | image: pepperlabs/peppermint:latest 26 | ports: 27 | - 3000:3000 28 | - 5003:5003 29 | restart: always 30 | depends_on: 31 | - peppermint_postgres 32 | environment: 33 | DB_USERNAME: "peppermint" 34 | DB_PASSWORD: "1234" 35 | DB_HOST: "peppermint_postgres" 36 | SECRET: 'peppermint4life' 37 | 38 | volumes: 39 | pgdata: 40 | ``` 41 | 42 | After you have created the docker-compose.yml file, run the following command: 43 | 44 | ```bash 45 | docker-compose up -d 46 | ``` 47 | 48 | Then you can access the application at http://your-server-ip:3000 49 | 50 | The default login credentials for the admin account are: 51 | 52 | ``` 53 | admin@admin.com 54 | 1234 55 | ``` 56 | -------------------------------------------------------------------------------- /apps/docs/pages/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to Peppermint 🍵 2 | 3 | Introducing Peppermint, a fully open-source helpdesk solution designed to enhance the user experience for teams currently utilizing costly software alternatives. Our goal is to develop intuitive software that encompasses all the feature-rich components in premium solutions yet remains user-friendly. 4 | 5 | Welcome to the documentation for Peppermint.sh, an open-source ticket management platform that empowers you to manage your data effectively and deliver top-tier client support. Explore this documentation to understand how to leverage Peppermint.sh efficiently and develop extensions for its functionality. 6 | 7 | This comprehensive guide covers initial setup, practical usage, and advanced development techniques, equipping you to maximize Peppermint's potential. 8 | -------------------------------------------------------------------------------- /apps/docs/pages/installer.md: -------------------------------------------------------------------------------- 1 | # Easy Installer 2 | 3 | Welcome to the Easy Installer page! This tool, known as **Spearmint**, is designed to simplify your setup process. 4 | 5 | ## What is Spearmint? 6 | 7 | [Spearmint](https://spearmint.sh/) is a streamlined installer created by the talented developer [Sydney, also known as sydmae on Discord](https://syd.gg/). This tool is built with user-friendliness in mind and aims to make installations more accessible. 8 | 9 | > **Note:** The Spearmint installer may occasionally be out of date or experience issues. While **Peppermint** supports and endorses this tool, we cannot guarantee its full functionality or safety. Use it at your own discretion, and remember to exercise caution during installation. 10 | 11 | --- 12 | 13 | ## Official Installation Method 14 | 15 | For the official method of installing Peppermint, please refer to the [Docker install page](https://docs.peppermint.sh/docker). This page provides a reliable and maintained guide to ensure a stable installation of Peppermint. 16 | -------------------------------------------------------------------------------- /apps/docs/pages/oidc.md: -------------------------------------------------------------------------------- 1 | ### How to Use OpenID Connect (OIDC) for Authentication 2 | 3 | This guide will walk you through the process of using OpenID Connect (OIDC) for authentication in your application. As an end user, you don't need to worry about the underlying code; just follow these steps to get started. 4 | 5 | ## Requirements 6 | - Set client type to PUBLIC in your oidc provider 7 | - OIDC well known config url 8 | - OIDC client ID 9 | 10 | 11 | #### Step 1: Logging In with OIDC 12 | 13 | - Go to the login page of the application. You should see options for logging in with different methods, including OIDC. 14 | 15 | - Click on the OIDC login button. This will redirect you to the OIDC provider's login page. 16 | 17 | - Enter your credentials on the OIDC provider's login page. This could be your email and password or any other authentication method supported by the provider. 18 | 19 | - After successful authentication, you will be redirected back to the application. If this is your first login, you might be taken to an onboarding page. 20 | 21 | #### Step 2: Managing OIDC Settings (Admin Only) 22 | 23 | If you are an admin, you can manage OIDC settings in the admin panel. 24 | 25 | - Log in to the application with admin credentials and navigate to the admin panel. 26 | 27 | - In the authentication settings section, select "OIDC" as the provider type. 28 | - Enter the necessary details such as the Issuer, Client ID, and Redirect URI. 29 | - The Issuer is the URL of the OIDC provider, it needs to be the well known configuration endpoint of the OIDC provider. 30 | 31 | - After entering the details, click the "Save" button to update the OIDC configuration. 32 | 33 | - If you need to remove the OIDC configuration, you can do so by clicking the "Delete" button in the admin panel. 34 | 35 | #### Step 3: Troubleshooting 36 | 37 | - **Account Not Found:** If you encounter an error stating "Account Not Found," it means your account might not be set up for OIDC. Contact your admin for assistance. 38 | 39 | - **Error During Login:** If there is an error during login, try again. If the issue persists, contact support for help. 40 | 41 | By following these steps, you can easily use OIDC for authentication in your application. If you have any questions or need further assistance, feel free to reach out to your admin or support team. 42 | -------------------------------------------------------------------------------- /apps/docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/apps/docs/public/favicon.ico -------------------------------------------------------------------------------- /apps/docs/seo.config.js: -------------------------------------------------------------------------------- 1 | const seoConfig = { 2 | metadataBase: new URL("https://peppermint.sh"), 3 | title: { 4 | template: "Peppermint", 5 | default: 6 | "Peppermint - Revolutionizing Customer Support for Rapid Resolutions. Your Premier Zendesk Alternative.", 7 | }, 8 | description: 9 | "Experience Peppermint's revolutionary approach to customer support, ensuring swift resolutions. Discover your ultimate alternative to Zendesk.", 10 | themeColor: "#F6E458", 11 | openGraph: { 12 | images: "/og-image.png", 13 | url: "https://peppermint.sh", 14 | }, 15 | manifest: "/site.webmanifest", 16 | icons: [ 17 | { rel: "icon", url: "/favicon.ico" }, 18 | { rel: "apple-touch-icon", url: "/apple-touch-icon.png" }, 19 | { rel: "mask-icon", url: "/favicon.ico" }, 20 | { rel: "image/x-icon", url: "/favicon.ico" }, 21 | ], 22 | twitter: { 23 | site: "@potts_dev", 24 | creator: "@potts_dev", 25 | }, 26 | }; 27 | 28 | export default seoConfig; -------------------------------------------------------------------------------- /apps/docs/theme.config.jsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router' 2 | import seoConfig from './seo.config.js' 3 | 4 | const config = { 5 | logo: "Peppermint", 6 | project: { link: 'https://github.com/Peppermint-Lab/peppermint' }, 7 | useNextSeoProps() { 8 | const { route } = useRouter() 9 | const { url, images } = seoConfig.openGraph 10 | 11 | return { 12 | titleTemplate: seoConfig.title.template, 13 | openGraph: { url, images: [{ url: `${url}${images}` }] } 14 | } 15 | }, 16 | docsRepositoryBase: 'https://github.com/Peppermint-Lab/docs', 17 | sidebar: { 18 | defaultMenuCollapseLevel: 2, 19 | toggleButton: false, 20 | }, 21 | chat: { 22 | link: 'https://discord.gg/X9yFbcV2rF', 23 | }, 24 | // i18n: [ 25 | // { locale: 'en', text: 'English' }, 26 | // ], 27 | footer: { 28 | text: '', 29 | component: () => <> 30 | }, 31 | banner: { 32 | key: 'release', 33 | text: ( 34 | 35 | 🎉 Peppermint 0.4.5 is here! Check it out now! 🚀 36 | 37 | ) 38 | }, 39 | head: () => { 40 | const title = seoConfig.title.template 41 | 42 | return ( 43 | <> 44 | {seoConfig.icons.map((icon, index) => ( 45 | 46 | ))} 47 | 48 | 52 | 56 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ) 68 | } 69 | } 70 | 71 | export default config -------------------------------------------------------------------------------- /apps/landing/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "react/no-unescaped-entities": "off", 5 | "@next/next/no-page-custom-font": "off", 6 | "@typescript-eslint/ban-ts-ignore": "off", 7 | "compilerOptions": { 8 | "skipDefaultLibCheck": true 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/landing/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for commiting if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /apps/landing/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /apps/landing/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /apps/landing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "landing", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "fathom-client": "^3.7.2", 13 | "lucide-react": "^0.454.0", 14 | "next": "15.0.2", 15 | "react": "19.0.0-rc-02c0e824-20241028", 16 | "react-dom": "19.0.0-rc-02c0e824-20241028" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^20", 20 | "@types/react": "^18", 21 | "@types/react-dom": "^18", 22 | "eslint": "^8", 23 | "eslint-config-next": "15.0.2", 24 | "postcss": "^8", 25 | "tailwindcss": "^3.4.1", 26 | "typescript": "^5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/landing/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /apps/landing/public/dashboard.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/apps/landing/public/dashboard.jpeg -------------------------------------------------------------------------------- /apps/landing/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/apps/landing/public/favicon.ico -------------------------------------------------------------------------------- /apps/landing/public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/landing/src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/apps/landing/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /apps/landing/src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/apps/landing/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /apps/landing/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | -------------------------------------------------------------------------------- /apps/landing/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "./globals.css"; 3 | import Fathom from "@/component/Fathom"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Peppermint", 7 | description: 8 | "Peppermint is a self-hosted issue tracker for your projects or help desk.", 9 | }; 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: Readonly<{ 14 | children: React.ReactNode; 15 | }>) { 16 | return ( 17 | 18 | 19 | 20 | {children} 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/landing/src/component/Fathom.tsx: -------------------------------------------------------------------------------- 1 | // Fathom.tsx 2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 3 | // @ts-nocheck 4 | "use client"; 5 | 6 | 7 | import { load, trackPageview } from "fathom-client"; 8 | import { useEffect, Suspense } from "react"; 9 | import { usePathname, useSearchParams } from "next/navigation"; 10 | 11 | function TrackPageView() { 12 | const pathname = usePathname(); 13 | const searchParams = useSearchParams(); 14 | 15 | // Load the Fathom script on mount 16 | useEffect(() => { 17 | load(process.env.NEXT_PUBLIC_FATHOM_ID, { 18 | auto: false, 19 | }); 20 | }, []); 21 | 22 | // Record a pageview when route changes 23 | useEffect(() => { 24 | if (!pathname) return; 25 | 26 | trackPageview({ 27 | url: pathname + searchParams?.toString(), 28 | referrer: document.referrer, 29 | }); 30 | }, [pathname, searchParams]); 31 | 32 | return null; 33 | } 34 | 35 | export default function Fathom() { 36 | return ( 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /apps/landing/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: "var(--background)", 13 | foreground: "var(--foreground)", 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | }; 19 | export default config; 20 | -------------------------------------------------------------------------------- /apps/landing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | peppermint_postgres: 5 | container_name: peppermint_postgres 6 | image: postgres:latest 7 | restart: always 8 | volumes: 9 | - pgdata:/var/lib/postgresql/data 10 | environment: 11 | POSTGRES_USER: peppermint 12 | POSTGRES_PASSWORD: 1234 13 | POSTGRES_DB: peppermint 14 | 15 | peppermint: 16 | container_name: peppermint 17 | image: pepperlabs/peppermint:test 18 | ports: 19 | - 3000:3000 20 | - 5003:5003 21 | restart: always 22 | depends_on: 23 | - peppermint_postgres 24 | environment: 25 | DB_USERNAME: "peppermint" 26 | DB_PASSWORD: "1234" 27 | DB_HOST: "peppermint_postgres" 28 | SECRET: 'peppermint4life' 29 | 30 | volumes: 31 | pgdata: -------------------------------------------------------------------------------- /docker-compose.local.yml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | peppermint_postgres: 5 | container_name: peppermint_postgres 6 | image: postgres:latest 7 | restart: always 8 | volumes: 9 | - pgdata:/var/lib/postgresql/data 10 | environment: 11 | POSTGRES_USER: peppermint 12 | POSTGRES_PASSWORD: 1234 13 | POSTGRES_DB: peppermint 14 | 15 | peppermint: 16 | container_name: peppermint 17 | build: "." 18 | ports: 19 | - 3000:3000 20 | - 5001:5003 21 | restart: always 22 | depends_on: 23 | - peppermint_postgres 24 | environment: 25 | DB_USERNAME: "peppermint" 26 | DB_PASSWORD: "1234" 27 | DB_HOST: "peppermint_postgres" 28 | SECRET: 'peppermint4life' 29 | 30 | volumes: 31 | pgdata: -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.1" 2 | 3 | services: 4 | peppermint_postgres: 5 | container_name: peppermint_postgres 6 | image: postgres:latest 7 | restart: always 8 | volumes: 9 | - pgdata_live:/var/lib/postgresql/data 10 | environment: 11 | POSTGRES_USER: peppermint 12 | POSTGRES_PASSWORD: 12345 13 | POSTGRES_DB: peppermint 14 | 15 | peppermint: 16 | container_name: peppermint 17 | image: pepperlabs/peppermint:latest 18 | ports: 19 | - 1000:3000 20 | - 1001:5003 21 | # If running via Docker on a hypervisor, enable DNS. 22 | # dns: 23 | # - 1.1.1.1 24 | # - 8.8.8.8 25 | restart: always 26 | depends_on: 27 | - peppermint_postgres 28 | environment: 29 | DB_USERNAME: "peppermint" 30 | DB_PASSWORD: "12345" 31 | DB_HOST: "peppermint_postgres" 32 | SECRET: 'peppermint4life' 33 | 34 | volumes: 35 | pgdata_live: 36 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts AS builder 2 | 3 | # Set the working directory inside the container 4 | WORKDIR /app 5 | 6 | RUN apt-get update && \ 7 | apt-get install -y build-essential python3 8 | 9 | # Copy the package.json and package-lock.json files for both apps 10 | COPY apps/api/package*.json ./apps/api/ 11 | COPY apps/client/package*.json ./apps/client/ 12 | COPY ./ecosystem.config.js ./ecosystem.config.js 13 | 14 | RUN npm i -g prisma 15 | RUN npm i -g typescript@latest -g --force 16 | 17 | # Copy the source code for both apps 18 | COPY apps/api ./apps/api 19 | COPY apps/client ./apps/client 20 | 21 | RUN cd apps/api && npm install --production 22 | RUN cd apps/api && npm i --save-dev @types/node && npm run build 23 | 24 | RUN cd apps/client && yarn install --production --ignore-scripts --prefer-offline --network-timeout 1000000 25 | RUN cd apps/client && yarn add --dev typescript @types/node --network-timeout 1000000 26 | RUN cd apps/client && yarn build 27 | 28 | FROM node:lts AS runner 29 | 30 | COPY --from=builder /app/apps/api/ ./apps/api/ 31 | COPY --from=builder /app/apps/client/.next/standalone ./apps/client 32 | COPY --from=builder /app/apps/client/.next/static ./apps/client/.next/static 33 | COPY --from=builder /app/apps/client/public ./apps/client/public 34 | COPY --from=builder /app/ecosystem.config.js ./ecosystem.config.js 35 | 36 | # Expose the ports for both apps 37 | EXPOSE 3000 5003 38 | 39 | # Install PM2 globally 40 | RUN npm install -g pm2 41 | 42 | # Start both apps using PM2 43 | CMD ["pm2-runtime", "ecosystem.config.js"] 44 | 45 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: "client", 5 | script: "node", 6 | args: "server.js", 7 | cwd: "apps/client", 8 | instances: "1", 9 | autorestart: true, 10 | watch: false, 11 | env: { 12 | NODE_ENV: "production", 13 | PORT: 3000, // Change this to your desired port 14 | }, 15 | }, 16 | { 17 | name: "api", 18 | script: "node", 19 | args: "dist/main.js", 20 | cwd: "apps/api", 21 | instances: "1", 22 | autorestart: true, 23 | watch: false, 24 | restart_delay: 3000, 25 | env: { 26 | NODE_ENV: "production", 27 | DB_USERNAME: process.env.DB_USERNAME, 28 | DB_PASSWORD: process.env.DB_PASSWORD, 29 | DB_HOST: process.env.DB_HOST, 30 | secret: process.env.SECRET, 31 | }, 32 | }, 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "peppermint", 4 | "scripts": { 5 | "build": "turbo run build", 6 | "dev": "turbo run dev --parallel", 7 | "lint": "turbo run lint", 8 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 9 | }, 10 | "devDependencies": { 11 | "@types/react": "18.2.38", 12 | "@types/react-dom": "^18", 13 | "prettier": "^2.5.1", 14 | "turbo": "^2.0.3" 15 | }, 16 | "workspaces": [ 17 | "apps/*", 18 | "packages/*" 19 | ], 20 | "packageManager": "yarn@4.2.2", 21 | "dependencies": { 22 | "fastify": "^5.1.0", 23 | "next": "^14.2.15", 24 | "nextra": "^3.0.15", 25 | "nextra-theme-docs": "^3.0.15", 26 | "react": "^18.3.1", 27 | "react-dom": "^18.3.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/config/eslint-preset.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next", "turbo", "prettier"], 3 | settings: { 4 | next: { 5 | rootDir: ["apps/*/", "packages/*/"], 6 | }, 7 | }, 8 | rules: { 9 | "@next/next/no-html-link-for-pages": "off", 10 | "react/jsx-key": "off", 11 | }, 12 | parserOptions: { 13 | babelOptions: { 14 | presets: [require.resolve("next/babel")], 15 | }, 16 | }, 17 | }; -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "config", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "files": [ 7 | "eslint-preset.js" 8 | ], 9 | "dependencies": { 10 | "eslint-config-next": "latest", 11 | "eslint-config-prettier": "^8.3.0", 12 | "eslint-config-turbo": "latest", 13 | "eslint-plugin-react": "7.28.0" 14 | }, 15 | "devDependencies": { 16 | "typescript": "^4.5.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "target": "es5", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "incremental": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve" 19 | }, 20 | "include": ["src", "next-env.d.ts"], 21 | "exclude": ["node_modules"] 22 | } -------------------------------------------------------------------------------- /packages/tsconfig/node16.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Node 16", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "lib": ["ES2021"], 7 | "module": "commonjs", 8 | "target": "ES2021", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true 13 | } 14 | } -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /static/create_a_ticket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/static/create_a_ticket.png -------------------------------------------------------------------------------- /static/detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/static/detail.png -------------------------------------------------------------------------------- /static/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/static/homepage.png -------------------------------------------------------------------------------- /static/tickets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Peppermint-Lab/peppermint/73abfd8fed53a4be94bd56f406f726ebaa947d95/static/tickets.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "app/client/pages/index.js", "app/client/pages/_app.js", "app/client/pages/swagger.js", "apps/api/src/lib/imap.js", "apps/docs/pages/_app.jsx", "apps/docs/pages/index.mdx", "apps/docs/theme.config.jsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "globalEnv": ["NODE_ENV"], 5 | "tasks": { 6 | "db:generate": { 7 | "cache": false 8 | }, 9 | "db:push": { 10 | "cache": false 11 | }, 12 | "build": { 13 | "outputs": ["dist/**", ".next/**"] 14 | }, 15 | "dev": { 16 | "cache": false, 17 | "persistent": true 18 | } 19 | } 20 | } 21 | --------------------------------------------------------------------------------