├── frontend └── svelte-kit │ ├── .npmrc │ ├── project.inlang │ ├── .gitignore │ ├── project_id │ └── settings.json │ ├── src │ ├── lib │ │ ├── components │ │ │ ├── ui │ │ │ │ ├── sonner │ │ │ │ │ ├── index.ts │ │ │ │ │ └── sonner.svelte │ │ │ │ ├── input │ │ │ │ │ ├── index.ts │ │ │ │ │ └── input.svelte │ │ │ │ ├── label │ │ │ │ │ ├── index.ts │ │ │ │ │ └── label.svelte │ │ │ │ ├── checkbox │ │ │ │ │ ├── index.ts │ │ │ │ │ └── checkbox.svelte │ │ │ │ ├── separator │ │ │ │ │ ├── index.ts │ │ │ │ │ └── separator.svelte │ │ │ │ ├── form │ │ │ │ │ ├── form-button.svelte │ │ │ │ │ ├── form-description.svelte │ │ │ │ │ ├── form-legend.svelte │ │ │ │ │ ├── form-label.svelte │ │ │ │ │ ├── form-fieldset.svelte │ │ │ │ │ ├── form-field-errors.svelte │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── form-field.svelte │ │ │ │ │ └── form-element-field.svelte │ │ │ │ ├── button │ │ │ │ │ └── index.ts │ │ │ │ ├── tabs │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── tabs-list.svelte │ │ │ │ │ ├── tabs-content.svelte │ │ │ │ │ └── tabs-trigger.svelte │ │ │ │ ├── sheet │ │ │ │ │ ├── sheet-title.svelte │ │ │ │ │ ├── sheet-description.svelte │ │ │ │ │ ├── sheet-header.svelte │ │ │ │ │ ├── sheet-overlay.svelte │ │ │ │ │ ├── sheet-footer.svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── dialog │ │ │ │ │ ├── dialog-description.svelte │ │ │ │ │ ├── dialog-title.svelte │ │ │ │ │ ├── dialog-header.svelte │ │ │ │ │ ├── dialog-overlay.svelte │ │ │ │ │ ├── dialog-footer.svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── select │ │ │ │ │ ├── select-group-heading.svelte │ │ │ │ │ ├── select-separator.svelte │ │ │ │ │ ├── select-scroll-up-button.svelte │ │ │ │ │ ├── select-scroll-down-button.svelte │ │ │ │ │ ├── select-trigger.svelte │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── select-item.svelte │ │ │ │ │ └── select-content.svelte │ │ │ │ ├── dropdown-menu │ │ │ │ │ ├── dropdown-menu-separator.svelte │ │ │ │ │ ├── dropdown-menu-group-heading.svelte │ │ │ │ │ ├── dropdown-menu-sub-content.svelte │ │ │ │ │ ├── dropdown-menu-shortcut.svelte │ │ │ │ │ ├── dropdown-menu-label.svelte │ │ │ │ │ ├── dropdown-menu-item.svelte │ │ │ │ │ ├── dropdown-menu-sub-trigger.svelte │ │ │ │ │ ├── dropdown-menu-radio-item.svelte │ │ │ │ │ ├── dropdown-menu-content.svelte │ │ │ │ │ └── dropdown-menu-checkbox-item.svelte │ │ │ │ ├── alert-dialog │ │ │ │ │ ├── alert-dialog-description.svelte │ │ │ │ │ ├── alert-dialog-title.svelte │ │ │ │ │ ├── alert-dialog-action.svelte │ │ │ │ │ ├── alert-dialog-cancel.svelte │ │ │ │ │ ├── alert-dialog-header.svelte │ │ │ │ │ ├── alert-dialog-footer.svelte │ │ │ │ │ ├── alert-dialog-overlay.svelte │ │ │ │ │ ├── index.ts │ │ │ │ │ └── alert-dialog-content.svelte │ │ │ │ ├── card │ │ │ │ │ ├── card-content.svelte │ │ │ │ │ ├── card-footer.svelte │ │ │ │ │ ├── card-header.svelte │ │ │ │ │ ├── card-description.svelte │ │ │ │ │ ├── card.svelte │ │ │ │ │ ├── index.ts │ │ │ │ │ └── card-title.svelte │ │ │ │ └── table │ │ │ │ │ ├── table-header.svelte │ │ │ │ │ ├── table-body.svelte │ │ │ │ │ ├── table-footer.svelte │ │ │ │ │ ├── table-caption.svelte │ │ │ │ │ ├── table.svelte │ │ │ │ │ ├── table-cell.svelte │ │ │ │ │ ├── table-row.svelte │ │ │ │ │ ├── table-head.svelte │ │ │ │ │ └── index.ts │ │ │ ├── shared │ │ │ │ └── loading.svelte │ │ │ ├── home │ │ │ │ ├── user-home.svelte │ │ │ │ └── guest-home.svelte │ │ │ └── navbar │ │ │ │ ├── theme-switcher.svelte │ │ │ │ ├── language-switcher.svelte │ │ │ │ └── guest-navbar.svelte │ │ ├── models │ │ │ ├── shared │ │ │ │ ├── availability.ts │ │ │ │ └── pageable.ts │ │ │ ├── auth │ │ │ │ ├── auth-tokens.ts │ │ │ │ └── jwt-payload.ts │ │ │ └── user │ │ │ │ ├── role.ts │ │ │ │ └── user.ts │ │ ├── utils.ts │ │ ├── i18n.ts │ │ └── regex.ts │ ├── hooks.ts │ ├── index.test.ts │ ├── routes │ │ ├── auth │ │ │ ├── forgot-password │ │ │ │ ├── schema.ts │ │ │ │ └── +page.server.ts │ │ │ ├── sign-in │ │ │ │ ├── schema.ts │ │ │ │ └── +page.server.ts │ │ │ ├── sign-out │ │ │ │ └── +server.ts │ │ │ ├── sign-out-from-all-devices │ │ │ │ └── +server.ts │ │ │ ├── reset-password │ │ │ │ ├── schema.ts │ │ │ │ └── +page.server.ts │ │ │ ├── verify-email │ │ │ │ └── +server.ts │ │ │ ├── resend-confirmation-email │ │ │ │ └── +server.ts │ │ │ └── sign-up │ │ │ │ └── schema.ts │ │ ├── profile │ │ │ ├── schema.ts │ │ │ ├── settings │ │ │ │ ├── schema.ts │ │ │ │ └── +page.svelte │ │ │ └── +page.server.ts │ │ ├── +page.svelte │ │ ├── user │ │ │ └── [name] │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.server.ts │ │ ├── +error.svelte │ │ ├── +layout.server.ts │ │ ├── +layout.svelte │ │ └── admin │ │ │ └── user │ │ │ └── schema.ts │ ├── error.html │ ├── app.html │ └── app.d.ts │ ├── static │ └── favicon.ico │ ├── postcss.config.js │ ├── .prettierignore │ ├── tests │ └── test.ts │ ├── .dockerignore │ ├── .gitignore │ ├── .prettierrc │ ├── playwright.config.ts │ ├── README.md │ ├── svelte.config.js │ ├── components.json │ ├── vite.config.ts │ ├── tsconfig.json │ ├── Dockerfile │ └── eslint.config.js ├── backend └── spring-boot │ ├── .dockerignore │ ├── src │ ├── main │ │ ├── resources │ │ │ ├── templates │ │ │ │ └── email │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── generate-html-emails.sh │ │ │ │ │ ├── reset-password.mjml │ │ │ │ │ └── verify-email.mjml │ │ │ ├── static │ │ │ │ └── favicon.ico │ │ │ ├── application-dev.yml │ │ │ ├── application-prod.yml │ │ │ ├── META-INF │ │ │ │ └── additional-spring-configuration-metadata.json │ │ │ ├── application.yml │ │ │ └── error-codes.properties │ │ └── java │ │ │ └── org │ │ │ └── bugzkit │ │ │ └── api │ │ │ ├── user │ │ │ ├── payload │ │ │ │ ├── dto │ │ │ │ │ ├── RoleDTO.java │ │ │ │ │ └── UserDTO.java │ │ │ │ └── request │ │ │ │ │ ├── EmailAvailabilityRequest.java │ │ │ │ │ ├── UsernameAvailabilityRequest.java │ │ │ │ │ ├── PatchProfileRequest.java │ │ │ │ │ └── ChangePasswordRequest.java │ │ │ ├── service │ │ │ │ ├── RoleService.java │ │ │ │ ├── ProfileService.java │ │ │ │ ├── UserService.java │ │ │ │ └── impl │ │ │ │ │ └── RoleServiceImpl.java │ │ │ ├── repository │ │ │ │ ├── RoleRepository.java │ │ │ │ └── UserRepository.java │ │ │ ├── controller │ │ │ │ └── RoleController.java │ │ │ ├── mapper │ │ │ │ └── UserMapper.java │ │ │ └── model │ │ │ │ └── Role.java │ │ │ ├── shared │ │ │ ├── payload │ │ │ │ └── dto │ │ │ │ │ ├── AvailabilityDTO.java │ │ │ │ │ └── PageableDTO.java │ │ │ ├── message │ │ │ │ └── service │ │ │ │ │ ├── MessageService.java │ │ │ │ │ └── impl │ │ │ │ │ └── MessageServiceImpl.java │ │ │ ├── email │ │ │ │ └── service │ │ │ │ │ ├── EmailService.java │ │ │ │ │ └── impl │ │ │ │ │ └── EmailServiceImpl.java │ │ │ ├── constants │ │ │ │ ├── Path.java │ │ │ │ └── Regex.java │ │ │ ├── generic │ │ │ │ └── crud │ │ │ │ │ └── CrudService.java │ │ │ ├── error │ │ │ │ ├── exception │ │ │ │ │ ├── ConflictException.java │ │ │ │ │ ├── BadRequestException.java │ │ │ │ │ ├── UnauthorizedException.java │ │ │ │ │ └── ResourceNotFoundException.java │ │ │ │ └── ErrorMessage.java │ │ │ ├── config │ │ │ │ ├── MessageSourceConfig.java │ │ │ │ ├── WebMvcConfig.java │ │ │ │ └── MailConfig.java │ │ │ ├── validator │ │ │ │ ├── impl │ │ │ │ │ ├── UsernameOrEmailImpl.java │ │ │ │ │ └── FieldMatchImpl.java │ │ │ │ ├── UsernameOrEmail.java │ │ │ │ └── FieldMatch.java │ │ │ ├── logger │ │ │ │ └── AspectLogger.java │ │ │ └── interceptor │ │ │ │ └── RequestInterceptor.java │ │ │ ├── auth │ │ │ ├── payload │ │ │ │ ├── dto │ │ │ │ │ └── AuthTokensDTO.java │ │ │ │ └── request │ │ │ │ │ ├── VerifyEmailRequest.java │ │ │ │ │ ├── VerificationEmailRequest.java │ │ │ │ │ ├── ForgotPasswordRequest.java │ │ │ │ │ ├── AuthTokensRequest.java │ │ │ │ │ ├── ResetPasswordRequest.java │ │ │ │ │ └── RegisterUserRequest.java │ │ │ ├── jwt │ │ │ │ ├── redis │ │ │ │ │ ├── repository │ │ │ │ │ │ ├── UserBlacklistRepository.java │ │ │ │ │ │ ├── AccessTokenBlacklistRepository.java │ │ │ │ │ │ └── RefreshTokenStoreRepository.java │ │ │ │ │ └── model │ │ │ │ │ │ ├── AccessTokenBlacklist.java │ │ │ │ │ │ ├── RefreshTokenStore.java │ │ │ │ │ │ └── UserBlacklist.java │ │ │ │ ├── service │ │ │ │ │ ├── VerificationTokenService.java │ │ │ │ │ ├── ResetPasswordTokenService.java │ │ │ │ │ ├── AccessTokenService.java │ │ │ │ │ └── RefreshTokenService.java │ │ │ │ ├── event │ │ │ │ │ ├── email │ │ │ │ │ │ ├── JwtEmail.java │ │ │ │ │ │ ├── JwtEmailSupplier.java │ │ │ │ │ │ ├── VerificationEmail.java │ │ │ │ │ │ └── ResetPasswordEmail.java │ │ │ │ │ ├── OnSendJwtEmail.java │ │ │ │ │ └── listener │ │ │ │ │ │ └── OnSendJwtEmailListener.java │ │ │ │ └── util │ │ │ │ │ └── JwtUtil.java │ │ │ ├── oauth2 │ │ │ │ └── OAuth2UserPrincipal.java │ │ │ ├── service │ │ │ │ └── AuthService.java │ │ │ └── security │ │ │ │ └── UserDetailsServiceImpl.java │ │ │ ├── Application.java │ │ │ └── admin │ │ │ ├── service │ │ │ └── UserService.java │ │ │ ├── payload │ │ │ └── request │ │ │ │ ├── PatchUserRequest.java │ │ │ │ └── UserRequest.java │ │ │ └── controller │ │ │ └── UserController.java │ └── test │ │ └── java │ │ └── org │ │ └── bugzkit │ │ └── api │ │ ├── shared │ │ ├── config │ │ │ └── DatabaseContainers.java │ │ ├── unit │ │ │ ├── MessageServiceTest.java │ │ │ └── EmailServiceTest.java │ │ └── util │ │ │ └── IntegrationTestUtil.java │ │ └── user │ │ └── data │ │ └── RoleRepositoryIT.java │ ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties │ ├── .gitignore │ ├── .run │ ├── UnitTests.run.xml │ ├── DataTests.run.xml │ ├── Application.run.xml │ ├── IntegrationTests.run.xml │ └── AllTests.run.xml │ ├── Dockerfile │ └── README.md ├── docs ├── src │ ├── content │ │ ├── deploy.mdx │ │ ├── getting-started │ │ │ ├── _meta.js │ │ │ └── running.mdx │ │ ├── features.mdx │ │ ├── how-it-works │ │ │ ├── _meta.js │ │ │ ├── introduction.mdx │ │ │ ├── email.mdx │ │ │ └── project-structure.mdx │ │ ├── _meta.js │ │ └── ci.mdx │ ├── app │ │ ├── favicon.ico │ │ ├── [[...mdxPath]] │ │ │ └── page.jsx │ │ └── layout.jsx │ └── mdx-components.js ├── .prettierrc ├── .prettierignore ├── README.md ├── next.config.ts ├── eslint.config.mjs ├── .gitignore ├── tsconfig.json └── package.json ├── .gitignore ├── .github └── workflows │ ├── docs.yml │ └── deploy.yml └── LICENSE /frontend/svelte-kit/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /frontend/svelte-kit/project.inlang/.gitignore: -------------------------------------------------------------------------------- 1 | cache -------------------------------------------------------------------------------- /backend/spring-boot/.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !src/ 3 | !pom.xml 4 | target/ -------------------------------------------------------------------------------- /backend/spring-boot/src/main/resources/templates/email/.gitignore: -------------------------------------------------------------------------------- 1 | *.html -------------------------------------------------------------------------------- /docs/src/content/deploy.mdx: -------------------------------------------------------------------------------- 1 | ## Deploy 2 | 3 | **This page is under construction.** 4 | -------------------------------------------------------------------------------- /docs/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/while1618/bugzkit/HEAD/docs/src/app/favicon.ico -------------------------------------------------------------------------------- /docs/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 100, 4 | "endOfLine": "auto" 5 | } 6 | -------------------------------------------------------------------------------- /frontend/svelte-kit/project.inlang/project_id: -------------------------------------------------------------------------------- 1 | ad545c0e7a9e1cce72d5c3b9b2aba07cc79ec94baea8316eb4ed74dd70aa39a6 -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/sonner/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Toaster } from './sonner.svelte'; 2 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/models/shared/availability.ts: -------------------------------------------------------------------------------- 1 | export interface Availability { 2 | available: boolean; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/models/shared/pageable.ts: -------------------------------------------------------------------------------- 1 | export interface Pageable { 2 | data: T[]; 3 | total: number; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/svelte-kit/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/while1618/bugzkit/HEAD/frontend/svelte-kit/static/favicon.ico -------------------------------------------------------------------------------- /frontend/svelte-kit/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /backend/spring-boot/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/while1618/bugzkit/HEAD/backend/spring-boot/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /docs/src/content/getting-started/_meta.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | running: 'Running', 3 | using: 'Using', 4 | }; 5 | 6 | export default config; 7 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/models/auth/auth-tokens.ts: -------------------------------------------------------------------------------- 1 | export interface AuthTokens { 2 | accessToken: string; 3 | refreshToken: string; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './input.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Input, 7 | }; 8 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './label.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Label, 7 | }; 8 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/while1618/bugzkit/HEAD/backend/spring-boot/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './checkbox.svelte'; 2 | export { 3 | Root, 4 | // 5 | Root as Checkbox, 6 | }; 7 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/user/payload/dto/RoleDTO.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.payload.dto; 2 | 3 | public record RoleDTO(String name) {} 4 | -------------------------------------------------------------------------------- /frontend/svelte-kit/.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | 6 | .vscode/ 7 | project.inlang/ 8 | src/lib/paraglide/ -------------------------------------------------------------------------------- /frontend/svelte-kit/tests/test.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | 3 | test('go to home page', async ({ page }) => { 4 | await page.goto('/'); 5 | }); 6 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/separator/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './separator.svelte'; 2 | 3 | export { 4 | Root, 5 | // 6 | Root as Separator, 7 | }; 8 | -------------------------------------------------------------------------------- /docs/.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | 6 | .vscode/ 7 | 8 | node_modules/ 9 | .next/ 10 | next-env.d.ts 11 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/models/user/role.ts: -------------------------------------------------------------------------------- 1 | export interface Role { 2 | name: RoleName; 3 | } 4 | 5 | export enum RoleName { 6 | USER = 'USER', 7 | ADMIN = 'ADMIN', 8 | } 9 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/payload/dto/AvailabilityDTO.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.payload.dto; 2 | 3 | public record AvailabilityDTO(boolean available) {} 4 | -------------------------------------------------------------------------------- /docs/src/content/features.mdx: -------------------------------------------------------------------------------- 1 | ## Incoming Features 2 | 3 | - [ ] Sign in with Google 4 | - [ ] Frontend in other frameworks - next, nuxt 5 | - [ ] Monitoring system - Grafana and Prometheus 6 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/hooks.ts: -------------------------------------------------------------------------------- 1 | // file initialized by the Paraglide-SvelteKit CLI - Feel free to edit it 2 | import { i18n } from '$lib/i18n'; 3 | 4 | export const reroute = i18n.reroute(); 5 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/payload/dto/AuthTokensDTO.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.payload.dto; 2 | 3 | public record AuthTokensDTO(String accessToken, String refreshToken) {} 4 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## bugzkit docs 2 | 3 | Docs for `bugzkit`, generated with [nextra](https://github.com/shuding/nextra). 4 | 5 | ### Running locally 6 | 7 | ```bash 8 | pnpm i 9 | pnpm run dev 10 | ``` 11 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /docs/src/content/how-it-works/_meta.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | introduction: 'Introduction', 3 | 'project-structure': 'Project Structure', 4 | auth: 'Auth', 5 | email: 'Email', 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/message/service/MessageService.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.message.service; 2 | 3 | public interface MessageService { 4 | String getMessage(String code); 5 | } 6 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/payload/dto/PageableDTO.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.payload.dto; 2 | 3 | import java.util.List; 4 | 5 | public record PageableDTO(List data, long total) {} 6 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/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 | -------------------------------------------------------------------------------- /backend/spring-boot/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/user/service/RoleService.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.service; 2 | 3 | import java.util.List; 4 | import org.bugzkit.api.user.payload.dto.RoleDTO; 5 | 6 | public interface RoleService { 7 | List findAll(); 8 | } 9 | -------------------------------------------------------------------------------- /frontend/svelte-kit/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | .git 4 | .gitignore 5 | .gitattributes 6 | README.md 7 | .npmrc 8 | .prettierrc 9 | eslint.config.js 10 | .prettierrc 11 | .prettierignore 12 | .svelte-kit 13 | .vscode 14 | node_modules 15 | build 16 | package 17 | **/.env -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/payload/request/VerifyEmailRequest.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.payload.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | 5 | public record VerifyEmailRequest(@NotBlank(message = "{auth.tokenRequired}") String token) {} 6 | -------------------------------------------------------------------------------- /docs/src/mdx-components.js: -------------------------------------------------------------------------------- 1 | import { useMDXComponents as getThemeComponents } from 'nextra-theme-docs'; 2 | 3 | const themeComponents = getThemeComponents(); 4 | 5 | export function useMDXComponents(components) { 6 | return { 7 | ...themeComponents, 8 | ...components, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/shared/loading.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 7 |
8 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/form/form-button.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | import nextra from 'nextra'; 3 | 4 | const nextConfig: NextConfig = { 5 | output: 'export', 6 | images: { 7 | unoptimized: true, 8 | }, 9 | }; 10 | 11 | const withNextra = nextra({}); 12 | 13 | export default withNextra(nextConfig); 14 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/i18n.ts: -------------------------------------------------------------------------------- 1 | // file initialized by the Paraglide-SvelteKit CLI - Feel free to edit it 2 | import * as runtime from '$lib/paraglide/runtime.js'; 3 | import { createI18n } from '@inlang/paraglide-sveltekit'; 4 | 5 | export const i18n = createI18n(runtime, { 6 | prefixDefaultLanguage: 'always', 7 | }); 8 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/auth/forgot-password/schema.ts: -------------------------------------------------------------------------------- 1 | import * as m from '$lib/paraglide/messages.js'; 2 | import { EMAIL_REGEX } from '$lib/regex'; 3 | import { z } from 'zod'; 4 | 5 | export const forgotPasswordSchema = z.object({ 6 | email: z.string().regex(EMAIL_REGEX, { message: m.auth_emailInvalid() }), 7 | }); 8 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/profile/schema.ts: -------------------------------------------------------------------------------- 1 | import * as m from '$lib/paraglide/messages.js'; 2 | import { USERNAME_REGEX } from '$lib/regex'; 3 | import { z } from 'zod'; 4 | 5 | export const setUsernameSchema = z.object({ 6 | username: z.string().regex(USERNAME_REGEX, { message: m.profile_usernameInvalid() }), 7 | }); 8 | -------------------------------------------------------------------------------- /docs/src/content/_meta.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | index: 'Introduction', 3 | 'getting-started': 'Getting Started', 4 | 'how-it-works': 'How It Works?', 5 | 'environment-variables': 'Environment Variables', 6 | ci: 'CI', 7 | deploy: 'Deploy', 8 | features: 'Incoming Features', 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/resources/templates/email/generate-html-emails.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | TEMPLATE_DIR="$(dirname "$0")" 3 | for file in "$TEMPLATE_DIR"/*.mjml; do 4 | if [ -f "$file" ]; then 5 | output="${file%.mjml}.html" 6 | echo "Converting $file to $output" 7 | npx mjml "$file" -o "$output" 8 | fi 9 | done 10 | -------------------------------------------------------------------------------- /frontend/svelte-kit/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | /.svelte-kit 7 | /build 8 | 9 | # OS 10 | .DS_Store 11 | Thumbs.db 12 | 13 | # Env 14 | .env 15 | .env.* 16 | !.env.example 17 | !.env.test 18 | 19 | # Vite 20 | vite.config.js.timestamp-* 21 | vite.config.ts.timestamp-* 22 | 23 | test-results 24 | -------------------------------------------------------------------------------- /frontend/svelte-kit/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 100, 4 | "endOfLine": "auto", 5 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 6 | "overrides": [ 7 | { 8 | "files": "*.svelte", 9 | "options": { 10 | "parser": "svelte" 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/repository/UserBlacklistRepository.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.redis.repository; 2 | 3 | import org.bugzkit.api.auth.jwt.redis.model.UserBlacklist; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | public interface UserBlacklistRepository extends CrudRepository {} 7 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/VerificationTokenService.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.service; 2 | 3 | import org.bugzkit.api.user.model.User; 4 | 5 | public interface VerificationTokenService { 6 | String create(Long userId); 7 | 8 | void check(String token); 9 | 10 | void sendToEmail(User user, String token); 11 | } 12 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/ResetPasswordTokenService.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.service; 2 | 3 | import org.bugzkit.api.user.model.User; 4 | 5 | public interface ResetPasswordTokenService { 6 | String create(Long userId); 7 | 8 | void check(String token); 9 | 10 | void sendToEmail(User user, String token); 11 | } 12 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %sveltekit.error.message% 6 | 7 | 8 | 9 |

My custom error page

10 |

Status: %sveltekit.status%

11 |

Message: %sveltekit.error.message%

12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import Root, { 2 | type ButtonProps, 3 | type ButtonSize, 4 | type ButtonVariant, 5 | buttonVariants, 6 | } from './button.svelte'; 7 | 8 | export { 9 | Root, 10 | type ButtonProps as Props, 11 | // 12 | Root as Button, 13 | buttonVariants, 14 | type ButtonProps, 15 | type ButtonSize, 16 | type ButtonVariant, 17 | }; 18 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/Application.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | public static void main(String[] args) { 9 | SpringApplication.run(Application.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/repository/AccessTokenBlacklistRepository.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.redis.repository; 2 | 3 | import org.bugzkit.api.auth.jwt.redis.model.AccessTokenBlacklist; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | public interface AccessTokenBlacklistRepository 7 | extends CrudRepository {} 8 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/email/service/EmailService.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.email.service; 2 | 3 | import jakarta.mail.MessagingException; 4 | import java.io.UnsupportedEncodingException; 5 | 6 | public interface EmailService { 7 | void sendHtmlEmail(String to, String subject, String body) 8 | throws MessagingException, UnsupportedEncodingException; 9 | } 10 | -------------------------------------------------------------------------------- /frontend/svelte-kit/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | 3 | const config: PlaywrightTestConfig = { 4 | webServer: { 5 | command: 'pnpm run build && pnpm run preview', 6 | port: 4173, 7 | }, 8 | use: { 9 | browserName: 'firefox', 10 | }, 11 | testDir: 'tests', 12 | testMatch: /(.+\.)?(test|spec)\.[jt]s/, 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | {#if data.profile} 10 | 11 | {:else} 12 | 13 | {/if} 14 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/constants/Path.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.constants; 2 | 3 | public class Path { 4 | public static final String AUTH = "/auth"; 5 | public static final String PROFILE = "/profile"; 6 | public static final String USERS = "/users"; 7 | public static final String ROLES = "/roles"; 8 | public static final String ADMIN_USERS = "/admin/users"; 9 | 10 | private Path() {} 11 | } 12 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/AccessTokenService.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.service; 2 | 3 | import java.util.Set; 4 | import org.bugzkit.api.user.payload.dto.RoleDTO; 5 | 6 | public interface AccessTokenService { 7 | String create(Long userId, Set roleDTOs); 8 | 9 | void check(String token); 10 | 11 | void invalidate(String token); 12 | 13 | void invalidateAllByUserId(Long userId); 14 | } 15 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/models/auth/jwt-payload.ts: -------------------------------------------------------------------------------- 1 | import type { RoleName } from '../user/role'; 2 | 3 | export interface JwtPayload { 4 | iss: string; 5 | iat: number; 6 | exp: number; 7 | purpose: JwtPurpose; 8 | roles?: RoleName[]; 9 | } 10 | 11 | export enum JwtPurpose { 12 | ACCESS_TOKEN = 'ACCESS_TOKEN', 13 | REFRESH_TOKEN = 'REFRESH_TOKEN', 14 | VERIFY_EMAIL_TOKEN = 'VERIFY_EMAIL_TOKEN', 15 | RESET_PASSWORD_TOKEN = 'RESET_PASSWORD_TOKEN', 16 | } 17 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/payload/request/VerificationEmailRequest.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.payload.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import org.bugzkit.api.shared.validator.UsernameOrEmail; 5 | 6 | public record VerificationEmailRequest( 7 | @NotBlank(message = "{user.usernameOrEmailRequired}") 8 | @UsernameOrEmail(message = "{user.usernameOrEmailInvalid}") 9 | String usernameOrEmail) {} 10 | -------------------------------------------------------------------------------- /frontend/svelte-kit/README.md: -------------------------------------------------------------------------------- 1 | ## Running the Frontend 2 | 3 | Ensure the API is up and running before starting the UI. Then, create a `.env` file in the root of the frontend directory with the following content: 4 | 5 | ```bash 6 | # Public 7 | PUBLIC_APP_NAME=your_app_name 8 | PUBLIC_API_URL=http://localhost:8080 9 | # Private 10 | JWT_SECRET=secret 11 | ``` 12 | 13 | To start the UI, run: 14 | 15 | ```bash 16 | pnpm install 17 | pnpm run dev 18 | ``` 19 | 20 | You're all set! 21 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/tabs/index.ts: -------------------------------------------------------------------------------- 1 | import { Tabs as TabsPrimitive } from 'bits-ui'; 2 | import Content from './tabs-content.svelte'; 3 | import List from './tabs-list.svelte'; 4 | import Trigger from './tabs-trigger.svelte'; 5 | 6 | const Root = TabsPrimitive.Root; 7 | 8 | export { 9 | Root, 10 | Content, 11 | List, 12 | Trigger, 13 | // 14 | Root as Tabs, 15 | Content as TabsContent, 16 | List as TabsList, 17 | Trigger as TabsTrigger, 18 | }; 19 | -------------------------------------------------------------------------------- /frontend/svelte-kit/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-node'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: [vitePreprocess()], 9 | 10 | kit: { 11 | adapter: adapter(), 12 | }, 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/sheet/sheet-title.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/payload/request/ForgotPasswordRequest.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.payload.request; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.NotBlank; 5 | import org.bugzkit.api.shared.constants.Regex; 6 | 7 | public record ForgotPasswordRequest( 8 | @NotBlank(message = "{user.emailRequired}") 9 | @Email(message = "{user.emailInvalid}", regexp = Regex.EMAIL) 10 | String email) {} 11 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/sheet/sheet-description.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/user/payload/request/EmailAvailabilityRequest.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.payload.request; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.NotBlank; 5 | import org.bugzkit.api.shared.constants.Regex; 6 | 7 | public record EmailAvailabilityRequest( 8 | @NotBlank(message = "{user.emailRequired}") 9 | @Email(message = "{user.emailInvalid}", regexp = Regex.EMAIL) 10 | String email) {} 11 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/app.d.ts: -------------------------------------------------------------------------------- 1 | import type { ParaglideLocals } from '@inlang/paraglide-sveltekit'; 2 | import type { AvailableLanguageTag } from '../../lib/paraglide/runtime'; 3 | // See https://kit.svelte.dev/docs/types#app 4 | 5 | declare global { 6 | namespace App { 7 | interface Locals { 8 | userId: string | null; 9 | paraglide: ParaglideLocals; 10 | } 11 | interface Error { 12 | message: string; 13 | } 14 | } 15 | } 16 | 17 | export {}; 18 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dialog/dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dialog/dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/select/select-group-heading.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/admin/service/UserService.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.admin.service; 2 | 3 | import org.bugzkit.api.admin.payload.request.PatchUserRequest; 4 | import org.bugzkit.api.admin.payload.request.UserRequest; 5 | import org.bugzkit.api.shared.generic.crud.CrudService; 6 | import org.bugzkit.api.user.payload.dto.UserDTO; 7 | 8 | public interface UserService extends CrudService { 9 | UserDTO patch(Long id, PatchUserRequest patchUserRequest); 10 | } 11 | -------------------------------------------------------------------------------- /frontend/svelte-kit/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://next.shadcn-svelte.com/schema.json", 3 | "style": "new-york", 4 | "tailwind": { 5 | "config": "tailwind.config.ts", 6 | "css": "src/app.css", 7 | "baseColor": "neutral" 8 | }, 9 | "aliases": { 10 | "components": "$lib/components", 11 | "utils": "$lib/utils", 12 | "ui": "$lib/components/ui", 13 | "hooks": "$lib/hooks" 14 | }, 15 | "typescript": true, 16 | "registry": "https://next.shadcn-svelte.com/registry" 17 | } 18 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/tabs/tabs-list.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/user/payload/request/UsernameAvailabilityRequest.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.payload.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import jakarta.validation.constraints.Pattern; 5 | import org.bugzkit.api.shared.constants.Regex; 6 | 7 | public record UsernameAvailabilityRequest( 8 | @NotBlank(message = "{user.usernameRequired}") 9 | @Pattern(regexp = Regex.USERNAME, message = "{user.usernameInvalid}") 10 | String username) {} 11 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # IDEs and editors 4 | /.idea 5 | .project 6 | .classpath 7 | .c9/ 8 | *.launch 9 | .settings/ 10 | *.iws 11 | *.iml 12 | *.ipr 13 | *.sublime-workspace 14 | 15 | # IDE - VSCode 16 | .vscode 17 | 18 | # NetBeans 19 | /nbproject/private/ 20 | /nbbuild/ 21 | /dist/ 22 | /nbdist/ 23 | /.nb-gradle/ 24 | build/ 25 | 26 | # STS 27 | .apt_generated 28 | .classpath 29 | .factorypath 30 | .project 31 | .settings 32 | .springBeans 33 | .sts4-cache 34 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/select/select-separator.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/models/user/user.ts: -------------------------------------------------------------------------------- 1 | import type { Role } from './role'; 2 | 3 | export interface AdminUser { 4 | id: number; 5 | username: string; 6 | email: string; 7 | active: boolean; 8 | lock: boolean; 9 | createdAt: Date; 10 | roles: Role[]; 11 | } 12 | 13 | export interface SimplifiedUser { 14 | id: number; 15 | username: string; 16 | createdAt: Date; 17 | } 18 | 19 | export interface Profile { 20 | id: number; 21 | username: string; 22 | email: string; 23 | createdAt: Date; 24 | } 25 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/auth/sign-in/schema.ts: -------------------------------------------------------------------------------- 1 | import * as m from '$lib/paraglide/messages.js'; 2 | import { EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX } from '$lib/regex'; 3 | import { z } from 'zod'; 4 | 5 | export const signInSchema = z.object({ 6 | usernameOrEmail: z 7 | .string() 8 | .refine( 9 | (value) => USERNAME_REGEX.test(value) || EMAIL_REGEX.test(value), 10 | m.auth_usernameOrEmailInvalid(), 11 | ), 12 | password: z.string().regex(PASSWORD_REGEX, { message: m.auth_passwordInvalid() }), 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/svelte-kit/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { paraglide } from '@inlang/paraglide-sveltekit/vite'; 2 | import { sveltekit } from '@sveltejs/kit/vite'; 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | export default defineConfig({ 6 | plugins: [paraglide({ project: './project.inlang', outdir: './src/lib/paraglide' }), sveltekit()], 7 | server: { 8 | port: process.env.PORT ? parseInt(process.env.PORT) : 5173, 9 | strictPort: true, 10 | }, 11 | test: { 12 | include: ['src/**/*.{test,spec}.{js,ts}'], 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /docs/src/content/how-it-works/introduction.mdx: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | The best way to get familiar with the project is to simply go through all the code that is written. 4 | If you find something that you don't understand, feel free to open an issue. 5 | 6 | Before diving into the code, the following sections will briefly cover key topics essential to understanding the project, including the reasoning why certain decisions were made. 7 | 8 | If you find anything that you believe should be done differently or seems incorrect, please open an issue for it. 9 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/card/card-content.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | {@render children?.()} 16 |
17 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/JwtEmail.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.event.email; 2 | 3 | import jakarta.mail.MessagingException; 4 | import java.io.IOException; 5 | import org.bugzkit.api.shared.email.service.EmailService; 6 | import org.bugzkit.api.user.model.User; 7 | import org.springframework.core.env.Environment; 8 | 9 | public interface JwtEmail { 10 | void sendEmail(EmailService emailService, Environment environment, User user, String token) 11 | throws IOException, MessagingException; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/form/form-description.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/label/label.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/generic/crud/CrudService.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.generic.crud; 2 | 3 | import org.bugzkit.api.shared.payload.dto.PageableDTO; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.stereotype.Service; 6 | 7 | @Service 8 | public interface CrudService { 9 | T create(U saveRequest); 10 | 11 | PageableDTO findAll(Pageable pageable); 12 | 13 | T findById(Long id); 14 | 15 | T update(Long id, U saveRequest); 16 | 17 | void delete(Long id); 18 | } 19 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/user/payload/request/PatchProfileRequest.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.payload.request; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.Pattern; 5 | import lombok.Builder; 6 | import org.bugzkit.api.shared.constants.Regex; 7 | 8 | @Builder 9 | public record PatchProfileRequest( 10 | @Pattern(regexp = Regex.USERNAME, message = "{user.usernameInvalid}") String username, 11 | @Email(message = "{user.emailInvalid}", regexp = Regex.EMAIL) String email) {} 12 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/user/service/ProfileService.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.service; 2 | 3 | import org.bugzkit.api.user.payload.dto.UserDTO; 4 | import org.bugzkit.api.user.payload.request.ChangePasswordRequest; 5 | import org.bugzkit.api.user.payload.request.PatchProfileRequest; 6 | 7 | public interface ProfileService { 8 | UserDTO find(); 9 | 10 | UserDTO patch(PatchProfileRequest patchProfileRequest); 11 | 12 | void delete(); 13 | 14 | void changePassword(ChangePasswordRequest changePasswordRequest); 15 | } 16 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/form/form-legend.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/user/repository/RoleRepository.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.repository; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import java.util.Set; 6 | import org.bugzkit.api.user.model.Role; 7 | import org.bugzkit.api.user.model.Role.RoleName; 8 | import org.springframework.data.jpa.repository.JpaRepository; 9 | 10 | public interface RoleRepository extends JpaRepository { 11 | List findAllByNameIn(Set roleNames); 12 | 13 | Optional findByName(RoleName roleName); 14 | } 15 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/card/card-footer.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | {@render children?.()} 16 |
17 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/table/table-header.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | {@render children?.()} 16 | 17 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/card/card-header.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | {@render children?.()} 16 |
17 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/error/exception/ConflictException.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.error.exception; 2 | 3 | import java.io.Serial; 4 | import lombok.Getter; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Getter 8 | public class ConflictException extends RuntimeException { 9 | @Serial private static final long serialVersionUID = 8841655774286844538L; 10 | private final HttpStatus status; 11 | 12 | public ConflictException(String message) { 13 | super(message); 14 | this.status = HttpStatus.CONFLICT; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/card/card-description.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |

15 | {@render children?.()} 16 |

17 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/table/table-body.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | {@render children?.()} 16 | 17 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/table/table-footer.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | {@render children?.()} 16 | 17 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/table/table-caption.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | {@render children?.()} 16 | 17 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/error/exception/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.error.exception; 2 | 3 | import java.io.Serial; 4 | import lombok.Getter; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Getter 8 | public class BadRequestException extends RuntimeException { 9 | @Serial private static final long serialVersionUID = -6237654540916338509L; 10 | private final HttpStatus status; 11 | 12 | public BadRequestException(String message) { 13 | super(message); 14 | this.status = HttpStatus.BAD_REQUEST; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/error/exception/UnauthorizedException.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.error.exception; 2 | 3 | import java.io.Serial; 4 | import lombok.Getter; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Getter 8 | public class UnauthorizedException extends RuntimeException { 9 | @Serial private static final long serialVersionUID = -8525346226722308705L; 10 | private final HttpStatus status; 11 | 12 | public UnauthorizedException(String message) { 13 | super(message); 14 | this.status = HttpStatus.UNAUTHORIZED; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/user/[name]/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 |
10 |
11 |
12 |

{data.user.username}

13 |

{m.home_info()}

14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/error/exception/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.error.exception; 2 | 3 | import java.io.Serial; 4 | import lombok.Getter; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Getter 8 | public class ResourceNotFoundException extends RuntimeException { 9 | @Serial private static final long serialVersionUID = -6147521296995365840L; 10 | private final HttpStatus status; 11 | 12 | public ResourceNotFoundException(String message) { 13 | super(message); 14 | this.status = HttpStatus.NOT_FOUND; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from '@eslint/eslintrc'; 2 | import { dirname } from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.config({ 14 | extends: ['next/core-web-vitals', 'next/typescript', 'prettier'], 15 | }), 16 | { 17 | ignores: ['node_modules/', '.next/', 'next-env.d.ts', 'out/'], 18 | }, 19 | ]; 20 | 21 | export default eslintConfig; 22 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/repository/RefreshTokenStoreRepository.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.redis.repository; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import org.bugzkit.api.auth.jwt.redis.model.RefreshTokenStore; 6 | import org.springframework.data.repository.CrudRepository; 7 | 8 | public interface RefreshTokenStoreRepository extends CrudRepository { 9 | Optional findByUserIdAndIpAddress(Long userId, String ipAddress); 10 | 11 | List findAllByUserId(Long userId); 12 | } 13 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/card/card.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/sheet/sheet-header.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/card/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './card.svelte'; 2 | import Content from './card-content.svelte'; 3 | import Description from './card-description.svelte'; 4 | import Footer from './card-footer.svelte'; 5 | import Header from './card-header.svelte'; 6 | import Title from './card-title.svelte'; 7 | 8 | export { 9 | Root, 10 | Content, 11 | Description, 12 | Footer, 13 | Header, 14 | Title, 15 | // 16 | Root as Card, 17 | Content as CardContent, 18 | Description as CardDescription, 19 | Footer as CardFooter, 20 | Header as CardHeader, 21 | Title as CardTitle, 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dialog/dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/sheet/sheet-overlay.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/tabs/tabs-content.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dialog/dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/sheet/sheet-footer.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/table/table.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | 16 | {@render children?.()} 17 |
18 |
19 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dialog/dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | {@render children?.()} 20 | 21 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/separator/separator.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/home/user-home.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 |
10 |
11 |
12 |

{m.home_greetings()}, {profile.username}

13 |

{m.home_info()}

14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
19 | {@render children?.()} 20 |
21 | -------------------------------------------------------------------------------- /frontend/svelte-kit/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/service/RefreshTokenService.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.service; 2 | 3 | import java.util.Optional; 4 | import java.util.Set; 5 | import org.bugzkit.api.user.payload.dto.RoleDTO; 6 | 7 | public interface RefreshTokenService { 8 | String create(Long userId, Set roleDTOs, String ipAddress); 9 | 10 | void check(String token); 11 | 12 | Optional findByUserIdAndIpAddress(Long userId, String ipAddress); 13 | 14 | void delete(String token); 15 | 16 | void deleteByUserIdAndIpAddress(Long userId, String ipAddress); 17 | 18 | void deleteAllByUserId(Long userId); 19 | } 20 | -------------------------------------------------------------------------------- /backend/spring-boot/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # IDEs and editors 4 | /.idea 5 | .project 6 | .classpath 7 | .c9/ 8 | *.launch 9 | .settings/ 10 | *.iws 11 | *.iml 12 | *.ipr 13 | *.sublime-workspace 14 | 15 | # IDE - VSCode 16 | .vscode 17 | 18 | # NetBeans 19 | /nbproject/private/ 20 | /nbbuild/ 21 | /dist/ 22 | /nbdist/ 23 | /.nb-gradle/ 24 | build/ 25 | 26 | # STS 27 | .apt_generated 28 | .classpath 29 | .factorypath 30 | .project 31 | .settings 32 | .springBeans 33 | .sts4-cache 34 | 35 | HELP.md 36 | target/ 37 | !.mvn/wrapper/maven-wrapper.jar 38 | !**/src/main/** 39 | !**/src/test/** 40 | .env 41 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/table/table-cell.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | [role=checkbox]]:translate-y-[2px]', 18 | className, 19 | )} 20 | {...restProps} 21 | > 22 | {@render children?.()} 23 | 24 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/table/table-row.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | {@render children?.()} 23 | 24 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 |
10 |
11 |

{page.status}

12 |

{page.error?.message}

13 | 14 |
15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
22 | {@render children?.()} 23 |
24 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/auth/sign-out/+server.ts: -------------------------------------------------------------------------------- 1 | import { makeRequest } from '$lib/server/apis/api'; 2 | import { HttpRequest, removeAuth } from '$lib/server/utils/util'; 3 | import { error, redirect } from '@sveltejs/kit'; 4 | import type { RequestHandler } from './$types'; 5 | 6 | export const GET = (async ({ locals, cookies }) => { 7 | const response = await makeRequest( 8 | { 9 | method: HttpRequest.DELETE, 10 | path: '/auth/tokens', 11 | }, 12 | cookies, 13 | ); 14 | 15 | if ('error' in response) error(response.status, { message: response.error }); 16 | 17 | removeAuth(cookies, locals); 18 | redirect(302, '/'); 19 | }) satisfies RequestHandler; 20 | -------------------------------------------------------------------------------- /docs/.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 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | _pagefind 44 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/navbar/theme-switcher.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/table/table-head.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | [role=checkbox]]:translate-y-[2px]', 18 | className, 19 | )} 20 | {...restProps} 21 | > 22 | {@render children?.()} 23 | 24 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/auth/sign-out-from-all-devices/+server.ts: -------------------------------------------------------------------------------- 1 | import { makeRequest } from '$lib/server/apis/api'; 2 | import { HttpRequest, removeAuth } from '$lib/server/utils/util'; 3 | import { error, redirect } from '@sveltejs/kit'; 4 | import type { RequestHandler } from './$types'; 5 | 6 | export const GET = (async ({ locals, cookies }) => { 7 | const response = await makeRequest( 8 | { 9 | method: HttpRequest.DELETE, 10 | path: '/auth/tokens/devices', 11 | }, 12 | cookies, 13 | ); 14 | 15 | if ('error' in response) error(response.status, { message: response.error }); 16 | 17 | removeAuth(cookies, locals); 18 | redirect(302, '/'); 19 | }) satisfies RequestHandler; 20 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/select/select-scroll-up-button.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/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 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/card/card-title.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
24 | {@render children?.()} 25 |
26 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/select/select-scroll-down-button.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/payload/request/AuthTokensRequest.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.payload.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import jakarta.validation.constraints.Pattern; 5 | import org.bugzkit.api.shared.constants.Regex; 6 | import org.bugzkit.api.shared.validator.UsernameOrEmail; 7 | 8 | public record AuthTokensRequest( 9 | @NotBlank(message = "{user.usernameOrEmailRequired}") 10 | @UsernameOrEmail(message = "{user.usernameOrEmailInvalid}") 11 | String usernameOrEmail, 12 | @NotBlank(message = "{user.passwordRequired}") 13 | @Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}") 14 | String password) {} 15 | -------------------------------------------------------------------------------- /backend/spring-boot/.run/UnitTests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/user/[name]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { SimplifiedUser } from '$lib/models/user/user'; 2 | import { makeRequest } from '$lib/server/apis/api'; 3 | import { HttpRequest } from '$lib/server/utils/util'; 4 | import { error } from '@sveltejs/kit'; 5 | import type { PageServerLoad } from './$types'; 6 | 7 | export const load = (async ({ params, cookies }) => { 8 | const response = await makeRequest( 9 | { 10 | method: HttpRequest.GET, 11 | path: `/users/username/${params.name}`, 12 | }, 13 | cookies, 14 | ); 15 | 16 | if ('error' in response) error(response.status, { message: response.error }); 17 | 18 | return { user: response as SimplifiedUser }; 19 | }) satisfies PageServerLoad; 20 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/config/MessageSourceConfig.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.config; 2 | 3 | import org.springframework.context.MessageSource; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.support.ReloadableResourceBundleMessageSource; 7 | 8 | @Configuration 9 | public class MessageSourceConfig { 10 | @Bean 11 | public MessageSource messageSource() { 12 | final var messageSource = new ReloadableResourceBundleMessageSource(); 13 | messageSource.setBasename("classpath:/error-codes"); 14 | messageSource.setDefaultEncoding("UTF-8"); 15 | return messageSource; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/form/form-label.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | {#snippet child({ props })} 17 | 20 | {/snippet} 21 | 22 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/auth/reset-password/schema.ts: -------------------------------------------------------------------------------- 1 | import * as m from '$lib/paraglide/messages.js'; 2 | import { PASSWORD_REGEX } from '$lib/regex'; 3 | import { z, ZodIssueCode } from 'zod'; 4 | 5 | export const resetPasswordSchema = z 6 | .object({ 7 | password: z.string().regex(PASSWORD_REGEX, { message: m.auth_passwordInvalid() }), 8 | confirmPassword: z.string().regex(PASSWORD_REGEX, { message: m.auth_passwordInvalid() }), 9 | }) 10 | .superRefine(({ password, confirmPassword }, ctx) => { 11 | if (password !== confirmPassword) { 12 | ctx.addIssue({ 13 | code: ZodIssueCode.custom, 14 | path: ['confirmPassword'], 15 | message: m.auth_passwordsDoNotMatch(), 16 | }); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /backend/spring-boot/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:sapmachine AS build 2 | RUN apt-get update; apt-get install -y curl \ 3 | && curl -sL https://deb.nodesource.com/setup_20.x | bash - \ 4 | && apt-get install -y nodejs \ 5 | && curl -L https://www.npmjs.com/install.sh | sh 6 | WORKDIR /app 7 | COPY pom.xml . 8 | COPY src ./src 9 | RUN mvn clean install -DskipTests 10 | 11 | FROM openjdk:21-jdk AS dev 12 | WORKDIR /app 13 | COPY --from=build /app/target/*.jar bugzkit-api.jar 14 | ENTRYPOINT ["java", "-Dspring.profiles.active=dev", "-jar", "bugzkit-api.jar"] 15 | 16 | FROM openjdk:21-jdk AS prod 17 | WORKDIR /app 18 | COPY --from=build /app/target/*.jar bugzkit-api.jar 19 | ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar", "bugzkit-api.jar"] 20 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/table/index.ts: -------------------------------------------------------------------------------- 1 | import Root from './table.svelte'; 2 | import Body from './table-body.svelte'; 3 | import Caption from './table-caption.svelte'; 4 | import Cell from './table-cell.svelte'; 5 | import Footer from './table-footer.svelte'; 6 | import Head from './table-head.svelte'; 7 | import Header from './table-header.svelte'; 8 | import Row from './table-row.svelte'; 9 | 10 | export { 11 | Root, 12 | Body, 13 | Caption, 14 | Cell, 15 | Footer, 16 | Head, 17 | Header, 18 | Row, 19 | // 20 | Root as Table, 21 | Body as TableBody, 22 | Caption as TableCaption, 23 | Cell as TableCell, 24 | Footer as TableFooter, 25 | Head as TableHead, 26 | Header as TableHeader, 27 | Row as TableRow, 28 | }; 29 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/home/guest-home.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |
8 |
9 |
10 |

{m.home_greetings()}

11 |

{m.home_info()}

12 |
13 | 14 | 15 |
16 |
17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /docs/src/content/getting-started/running.mdx: -------------------------------------------------------------------------------- 1 | ## Running 2 | 3 | To run `bugzkit` locally, all you need is Docker. Follow these steps: 4 | 5 | ```bash 6 | git clone https://github.com/while1618/bugzkit.git 7 | cd bugzkit 8 | docker-compose up --build -d 9 | ``` 10 | 11 | Access the application at these URLs: 12 | 13 | - **UI**: [http://localhost:5173](http://localhost:5173) 14 | - **API**: [http://localhost:8080/users](http://localhost:8080/users) 15 | - **API Docs**: [http://localhost:8080/swagger-ui/index.html](http://localhost:8080/swagger-ui/index.html) 16 | 17 | **Note**: SMTP and Google OAuth require configuration. Refer to the Environment Variables section for setup. 18 | 19 | ### Default Login Credentials 20 | 21 | - **Username**: user/admin 22 | - **Password**: qwerty123 23 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/form/form-fieldset.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/OnSendJwtEmail.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.event; 2 | 3 | import java.io.Serial; 4 | import lombok.Getter; 5 | import org.bugzkit.api.auth.jwt.util.JwtUtil.JwtPurpose; 6 | import org.bugzkit.api.user.model.User; 7 | import org.springframework.context.ApplicationEvent; 8 | 9 | @Getter 10 | public class OnSendJwtEmail extends ApplicationEvent { 11 | @Serial private static final long serialVersionUID = 6234594744610595282L; 12 | private final User user; 13 | private final String token; 14 | private final JwtPurpose purpose; 15 | 16 | public OnSendJwtEmail(User user, String token, JwtPurpose purpose) { 17 | super(user); 18 | this.user = user; 19 | this.token = token; 20 | this.purpose = purpose; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/message/service/impl/MessageServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.message.service.impl; 2 | 3 | import org.bugzkit.api.shared.message.service.MessageService; 4 | import org.springframework.context.MessageSource; 5 | import org.springframework.context.i18n.LocaleContextHolder; 6 | import org.springframework.stereotype.Service; 7 | 8 | @Service 9 | public class MessageServiceImpl implements MessageService { 10 | private final MessageSource messageSource; 11 | 12 | public MessageServiceImpl(MessageSource messageSource) { 13 | this.messageSource = messageSource; 14 | } 15 | 16 | @Override 17 | public String getMessage(String code) { 18 | return messageSource.getMessage(code, null, LocaleContextHolder.getLocale()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/auth/verify-email/+server.ts: -------------------------------------------------------------------------------- 1 | import { makeRequest } from '$lib/server/apis/api'; 2 | import { HttpRequest } from '$lib/server/utils/util'; 3 | import { error, redirect } from '@sveltejs/kit'; 4 | import type { RequestHandler } from './$types'; 5 | 6 | export const GET = (async ({ locals, url, cookies }) => { 7 | if (locals.userId) redirect(302, '/'); 8 | 9 | const token = url.searchParams.get('token'); 10 | const response = await makeRequest( 11 | { 12 | method: HttpRequest.POST, 13 | path: '/auth/verify-email', 14 | body: JSON.stringify({ token }), 15 | }, 16 | cookies, 17 | ); 18 | 19 | if ('error' in response) error(response.status, { message: response.error }); 20 | 21 | redirect(302, '/auth/sign-in'); 22 | }) satisfies RequestHandler; 23 | -------------------------------------------------------------------------------- /backend/spring-boot/.run/DataTests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/model/AccessTokenBlacklist.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.redis.model; 2 | 3 | import java.io.Serial; 4 | import java.io.Serializable; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import org.springframework.data.annotation.Id; 9 | import org.springframework.data.redis.core.RedisHash; 10 | import org.springframework.data.redis.core.TimeToLive; 11 | 12 | @Getter 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | @RedisHash(value = "AccessTokenBlacklist") 16 | public class AccessTokenBlacklist implements Serializable { 17 | @Serial private static final long serialVersionUID = 7371548317284111557L; 18 | 19 | @Id private String accessToken; 20 | 21 | @TimeToLive private long timeToLive; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/svelte-kit/project.inlang/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://inlang.com/schema/project-settings", 3 | "sourceLanguageTag": "en", 4 | "languageTags": [ 5 | "en", 6 | "sr" 7 | ], 8 | "modules": [ 9 | "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js", 10 | "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js", 11 | "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@latest/dist/index.js", 12 | "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js", 13 | "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js" 14 | ], 15 | "plugin.inlang.messageFormat": { 16 | "pathPattern": "./messages/{languageTag}.json" 17 | } 18 | } -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/sonner/sonner.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 22 | -------------------------------------------------------------------------------- /docs/src/app/[[...mdxPath]]/page.jsx: -------------------------------------------------------------------------------- 1 | import { generateStaticParamsFor, importPage } from 'nextra/pages'; 2 | import { useMDXComponents } from '../../mdx-components'; 3 | 4 | export const generateStaticParams = generateStaticParamsFor('mdxPath'); 5 | 6 | export async function generateMetadata(props) { 7 | const params = await props.params; 8 | const { metadata } = await importPage(params.mdxPath); 9 | return metadata; 10 | } 11 | 12 | const Wrapper = useMDXComponents().wrapper; 13 | 14 | export default async function Page(props) { 15 | const params = await props.params; 16 | const result = await importPage(params.mdxPath); 17 | const { default: MDXContent, toc, metadata } = result; 18 | return ( 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/validator/impl/UsernameOrEmailImpl.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.validator.impl; 2 | 3 | import jakarta.validation.ConstraintValidator; 4 | import jakarta.validation.ConstraintValidatorContext; 5 | import java.util.regex.Pattern; 6 | import org.bugzkit.api.shared.constants.Regex; 7 | import org.bugzkit.api.shared.validator.UsernameOrEmail; 8 | 9 | public class UsernameOrEmailImpl implements ConstraintValidator { 10 | public boolean isValid(String usernameOrEmail, ConstraintValidatorContext context) { 11 | return usernameOrEmail != null 12 | && !usernameOrEmail.isEmpty() 13 | && (Pattern.compile(Regex.USERNAME).matcher(usernameOrEmail).matches() 14 | || Pattern.compile(Regex.EMAIL).matcher(usernameOrEmail).matches()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/auth/resend-confirmation-email/+server.ts: -------------------------------------------------------------------------------- 1 | import { makeRequest } from '$lib/server/apis/api'; 2 | import { HttpRequest } from '$lib/server/utils/util'; 3 | import { error, redirect } from '@sveltejs/kit'; 4 | import type { RequestHandler } from './$types'; 5 | 6 | export const GET = (async ({ locals, url, cookies }) => { 7 | if (locals.userId) redirect(302, '/'); 8 | 9 | const usernameOrEmail = url.searchParams.get('usernameOrEmail'); 10 | const response = await makeRequest( 11 | { 12 | method: HttpRequest.POST, 13 | path: '/auth/verification-email', 14 | body: JSON.stringify({ usernameOrEmail }), 15 | }, 16 | cookies, 17 | ); 18 | 19 | if ('error' in response) error(response.status, { message: response.error }); 20 | 21 | redirect(302, '/auth/sign-in'); 22 | }) satisfies RequestHandler; 23 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | svg]:size-4 [&>svg]:shrink-0', 19 | inset && 'pl-8', 20 | className, 21 | )} 22 | {...restProps} 23 | /> 24 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/tabs/tabs-trigger.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs CI 2 | run-name: ${{ github.actor }} started CI 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | branches: [master] 8 | paths: 9 | - docs/** 10 | pull_request: 11 | branches: [master] 12 | paths: 13 | - docs/** 14 | 15 | jobs: 16 | build: 17 | defaults: 18 | run: 19 | working-directory: docs 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Set up Node.js 22 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: 22 31 | 32 | - name: Install pnpm 33 | uses: pnpm/action-setup@v4 34 | with: 35 | version: 9 36 | 37 | - name: Install dependencies 38 | run: pnpm i 39 | 40 | - name: Lint 41 | run: pnpm run lint 42 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/user/service/UserService.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.service; 2 | 3 | import org.bugzkit.api.shared.payload.dto.AvailabilityDTO; 4 | import org.bugzkit.api.shared.payload.dto.PageableDTO; 5 | import org.bugzkit.api.user.payload.dto.UserDTO; 6 | import org.bugzkit.api.user.payload.request.EmailAvailabilityRequest; 7 | import org.bugzkit.api.user.payload.request.UsernameAvailabilityRequest; 8 | import org.springframework.data.domain.Pageable; 9 | 10 | public interface UserService { 11 | PageableDTO findAll(Pageable pageable); 12 | 13 | UserDTO findById(Long id); 14 | 15 | UserDTO findByUsername(String username); 16 | 17 | AvailabilityDTO usernameAvailability(UsernameAvailabilityRequest usernameAvailabilityRequest); 18 | 19 | AvailabilityDTO emailAvailability(EmailAvailabilityRequest emailAvailabilityRequest); 20 | } 21 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | ui: 2 | url: ${UI_URL:http://localhost:5173} 3 | 4 | spring: 5 | datasource: 6 | url: jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/${POSTGRES_DATABASE:bugzkit} 7 | password: ${POSTGRES_PASSWORD:root} 8 | jpa: 9 | hibernate: 10 | ddl-auto: create 11 | data: 12 | redis: 13 | host: ${REDIS_HOST:localhost} 14 | password: ${REDIS_PASSWORD:root} 15 | security: 16 | user: 17 | password: ${USER_PASSWORD:qwerty123} 18 | oauth2: 19 | client: 20 | registration: 21 | google: 22 | client-id: ${GOOGLE_CLIENT_ID:client_id} 23 | client-secret: ${GOOGLE_CLIENT_SECRET:client_secret} 24 | scope: profile,email 25 | mail: 26 | password: ${SMTP_PASSWORD:password} 27 | 28 | jwt: 29 | secret: ${JWT_SECRET:secret} 30 | -------------------------------------------------------------------------------- /backend/spring-boot/README.md: -------------------------------------------------------------------------------- 1 | ## Running the Backend 2 | 3 | ### Running Locally 4 | 5 | For the API to start properly, you'll need Postgres and Redis running. The easies way to start them is via Docker. Just run `docker-compose-db.dev.yml`: 6 | 7 | ```bash 8 | docker-compose -f docker-compose-db.dev.yml up -d 9 | ``` 10 | 11 | After the services are up and running, start the application: 12 | 13 | ```bash 14 | mvn clean install 15 | mvn spring-boot:run -Dspring-boot.run.profiles=dev 16 | ``` 17 | 18 | Alternatively, use IntelliJ IDEA. A pre-configured run configuration is available - just click and run! 19 | 20 | ### Running via Docker 21 | 22 | For running the API via Docker, execute: 23 | 24 | ```bash 25 | docker-compose -f docker-compose-api.dev.yml up --build -d 26 | ``` 27 | 28 | **Note**: SMTP and Google OAuth require configuration. Refer to the Environment Variables section for setup. 29 | -------------------------------------------------------------------------------- /docs/src/content/how-it-works/email.mdx: -------------------------------------------------------------------------------- 1 | ## Email 2 | 3 | Sending emails requires additional setup and won't work out of the box. 4 | You'll need to configure your SMTP, provide host, username and password. 5 | You can set these up directly in the code or via environment variables. Refer to the corresponding section for more details. 6 | 7 | ### Adding your HTML templates 8 | 9 | Emails are send via SpringBoot, you can create HTML email template in the `resources/templates/email` directory and send them using the `sendHtmlEmail()` method from the `EmailService`. 10 | However, creating responsive emails can be challenging. 11 | 12 | To simplify this, I've integrated [MJML](https://github.com/mjmlio/mjml). 13 | To use it, create a `.mjml` file in the `resources/templates/email` directory. 14 | During the `mvn compile` phase, the `mjml` CLI will automatically generate the corresponding HTML email for you. 15 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/payload/request/ResetPasswordRequest.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.payload.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import jakarta.validation.constraints.Pattern; 5 | import org.bugzkit.api.shared.constants.Regex; 6 | import org.bugzkit.api.shared.validator.FieldMatch; 7 | 8 | @FieldMatch(first = "password", second = "confirmPassword", message = "{user.passwordsDoNotMatch}") 9 | public record ResetPasswordRequest( 10 | @NotBlank(message = "{auth.tokenRequired}") String token, 11 | @NotBlank(message = "{user.passwordRequired}") 12 | @Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}") 13 | String password, 14 | @NotBlank(message = "{user.passwordRequired}") 15 | @Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}") 16 | String confirmPassword) {} 17 | -------------------------------------------------------------------------------- /frontend/svelte-kit/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22-alpine AS base 2 | WORKDIR /app 3 | ARG PUBLIC_APP_NAME 4 | ARG PUBLIC_API_URL 5 | ENV PUBLIC_APP_NAME=$PUBLIC_APP_NAME 6 | ENV PUBLIC_API_URL=$PUBLIC_API_URL 7 | COPY package.json . 8 | COPY pnpm-lock.yaml . 9 | RUN npm install -g pnpm 10 | RUN pnpm install 11 | COPY . . 12 | 13 | FROM base AS dev 14 | RUN apk add --no-cache curl 15 | CMD ["pnpm", "run", "dev", "--host"] 16 | 17 | FROM base AS build 18 | RUN pnpm run build 19 | RUN pnpm prune --prod 20 | 21 | FROM node:22-alpine AS prod 22 | RUN apk add --no-cache curl 23 | WORKDIR /app 24 | # copy node_modules only if you have dependencies in package.json 25 | # if you only have devDependencies you can skip this line 26 | # COPY --from=build /app/node_modules node_modules/ 27 | COPY --from=build /app/build build/ 28 | COPY --from=build /app/package.json . 29 | ENV NODE_ENV=production 30 | CMD ["node", "build"] 31 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/input/input.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/JwtEmailSupplier.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.event.email; 2 | 3 | import java.util.EnumMap; 4 | import java.util.Map; 5 | import java.util.function.Supplier; 6 | import org.bugzkit.api.auth.jwt.util.JwtUtil.JwtPurpose; 7 | 8 | public class JwtEmailSupplier { 9 | private static final Map> emailType = 10 | new EnumMap<>(JwtPurpose.class); 11 | 12 | static { 13 | emailType.put(JwtPurpose.VERIFY_EMAIL_TOKEN, VerificationEmail::new); 14 | emailType.put(JwtPurpose.RESET_PASSWORD_TOKEN, ResetPasswordEmail::new); 15 | } 16 | 17 | public JwtEmail supplyEmail(JwtPurpose jwtPurpose) { 18 | final var emailSupplier = emailType.get(jwtPurpose); 19 | if (emailSupplier == null) 20 | throw new IllegalArgumentException("Invalid email type: " + jwtPurpose); 21 | return emailSupplier.get(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/auth/sign-up/schema.ts: -------------------------------------------------------------------------------- 1 | import * as m from '$lib/paraglide/messages.js'; 2 | import { EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX } from '$lib/regex'; 3 | import { z, ZodIssueCode } from 'zod'; 4 | 5 | export const signUpSchema = z 6 | .object({ 7 | username: z.string().regex(USERNAME_REGEX, { message: m.auth_usernameInvalid() }), 8 | email: z.string().regex(EMAIL_REGEX, { message: m.auth_emailInvalid() }), 9 | password: z.string().regex(PASSWORD_REGEX, { message: m.auth_passwordInvalid() }), 10 | confirmPassword: z.string().regex(PASSWORD_REGEX, { message: m.auth_passwordInvalid() }), 11 | }) 12 | .superRefine(({ password, confirmPassword }, ctx) => { 13 | if (password !== confirmPassword) { 14 | ctx.addIssue({ 15 | code: ZodIssueCode.custom, 16 | path: ['confirmPassword'], 17 | message: m.auth_passwordsDoNotMatch(), 18 | }); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /backend/spring-boot/.run/Application.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 11 | 22 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/user/controller/RoleController.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.controller; 2 | 3 | import java.util.List; 4 | import org.bugzkit.api.shared.constants.Path; 5 | import org.bugzkit.api.user.payload.dto.RoleDTO; 6 | import org.bugzkit.api.user.service.RoleService; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @RestController 13 | @RequestMapping(Path.ROLES) 14 | public class RoleController { 15 | private final RoleService roleService; 16 | 17 | public RoleController(RoleService roleService) { 18 | this.roleService = roleService; 19 | } 20 | 21 | @GetMapping 22 | public ResponseEntity> findAll() { 23 | return ResponseEntity.ok(roleService.findAll()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy CI 2 | run-name: ${{ github.actor }} started CI 3 | 4 | on: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Fail if not on master 16 | if: github.ref != 'refs/heads/master' 17 | run: echo "This workflow can only be run on the master branch." && exit 1 18 | 19 | - name: Fail if not run by owner 20 | if: github.actor != 'while1618' 21 | run: echo "This workflow can only be run by the owner." && exit 1 22 | 23 | - name: Docker Stack Deploy 24 | uses: cssnr/stack-deploy-action@v1 25 | with: 26 | name: bugzkit 27 | file: docker-stack.prod.yml 28 | host: ${{ secrets.HOST }} 29 | user: ${{ secrets.DEPLOY_USER }} 30 | ssh_key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }} 31 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/user/service/impl/RoleServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.service.impl; 2 | 3 | import java.util.List; 4 | import org.bugzkit.api.user.mapper.UserMapper; 5 | import org.bugzkit.api.user.payload.dto.RoleDTO; 6 | import org.bugzkit.api.user.repository.RoleRepository; 7 | import org.bugzkit.api.user.service.RoleService; 8 | import org.springframework.security.access.prepost.PreAuthorize; 9 | import org.springframework.stereotype.Service; 10 | 11 | @Service 12 | @PreAuthorize("hasAuthority('ADMIN')") 13 | public class RoleServiceImpl implements RoleService { 14 | private final RoleRepository roleRepository; 15 | 16 | public RoleServiceImpl(RoleRepository roleRepository) { 17 | this.roleRepository = roleRepository; 18 | } 19 | 20 | @Override 21 | public List findAll() { 22 | return roleRepository.findAll().stream().map(UserMapper.INSTANCE::roleToRoleDTO).toList(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/model/RefreshTokenStore.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.redis.model; 2 | 3 | import java.io.Serial; 4 | import java.io.Serializable; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import org.springframework.data.annotation.Id; 9 | import org.springframework.data.redis.core.RedisHash; 10 | import org.springframework.data.redis.core.TimeToLive; 11 | import org.springframework.data.redis.core.index.Indexed; 12 | 13 | @Getter 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | @RedisHash(value = "RefreshTokenStore") 17 | public class RefreshTokenStore implements Serializable { 18 | @Serial private static final long serialVersionUID = -1997218842142407911L; 19 | 20 | @Id private String refreshToken; 21 | 22 | @Indexed private Long userId; 23 | 24 | @Indexed private String ipAddress; 25 | 26 | @TimeToLive private long timeToLive; 27 | } 28 | -------------------------------------------------------------------------------- /backend/spring-boot/src/test/java/org/bugzkit/api/shared/config/DatabaseContainers.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.config; 2 | 3 | import org.springframework.boot.testcontainers.service.connection.ServiceConnection; 4 | import org.testcontainers.containers.GenericContainer; 5 | import org.testcontainers.containers.PostgreSQLContainer; 6 | import org.testcontainers.junit.jupiter.Container; 7 | import org.testcontainers.junit.jupiter.Testcontainers; 8 | import org.testcontainers.utility.DockerImageName; 9 | 10 | @Testcontainers 11 | public abstract class DatabaseContainers { 12 | @Container @ServiceConnection 13 | static PostgreSQLContainer postgres = 14 | new PostgreSQLContainer<>(DockerImageName.parse("postgres").withTag("latest")); 15 | 16 | @Container 17 | @ServiceConnection(name = "redis") 18 | static GenericContainer redis = 19 | new GenericContainer<>(DockerImageName.parse("redis").withTag("latest")) 20 | .withExposedPorts(6379); 21 | } 22 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/validator/UsernameOrEmail.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.validator; 2 | 3 | import static java.lang.annotation.ElementType.ANNOTATION_TYPE; 4 | import static java.lang.annotation.ElementType.FIELD; 5 | import static java.lang.annotation.ElementType.TYPE; 6 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 7 | 8 | import jakarta.validation.Constraint; 9 | import jakarta.validation.Payload; 10 | import java.lang.annotation.Documented; 11 | import java.lang.annotation.Retention; 12 | import java.lang.annotation.Target; 13 | import org.bugzkit.api.shared.validator.impl.UsernameOrEmailImpl; 14 | 15 | @Target({TYPE, FIELD, ANNOTATION_TYPE}) 16 | @Retention(RUNTIME) 17 | @Constraint(validatedBy = UsernameOrEmailImpl.class) 18 | @Documented 19 | public @interface UsernameOrEmail { 20 | String message() default ""; 21 | 22 | Class[] groups() default {}; 23 | 24 | Class[] payload() default {}; 25 | } 26 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/form/form-field-errors.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | {#snippet children({ errors, errorProps })} 22 | {#if childrenProp} 23 | {@render childrenProp({ errors, errorProps })} 24 | {:else} 25 | {#each errors as error} 26 |
{error}
27 | {/each} 28 | {/if} 29 | {/snippet} 30 |
31 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/redis/model/UserBlacklist.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.redis.model; 2 | 3 | import java.io.Serial; 4 | import java.io.Serializable; 5 | import java.time.Instant; 6 | import java.time.temporal.ChronoUnit; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Builder; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | import org.springframework.data.annotation.Id; 12 | import org.springframework.data.redis.core.RedisHash; 13 | import org.springframework.data.redis.core.TimeToLive; 14 | 15 | @Getter 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | @Builder 19 | @RedisHash(value = "UserBlacklist") 20 | public class UserBlacklist implements Serializable { 21 | @Serial private static final long serialVersionUID = 8334740937644612692L; 22 | 23 | @Id private Long userId; 24 | 25 | @Builder.Default private Instant updatedAt = Instant.now().truncatedTo(ChronoUnit.SECONDS); 26 | 27 | @TimeToLive private long timeToLive; 28 | } 29 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/select/select-trigger.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | span]:line-clamp-1', 18 | className, 19 | )} 20 | {...restProps} 21 | > 22 | {@render children?.()} 23 | 24 | 25 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/resources/application-prod.yml: -------------------------------------------------------------------------------- 1 | ui: 2 | url: ${UI_URL:https://bugzkit.com} 3 | 4 | server: 5 | forward-headers-strategy: native 6 | 7 | spring: 8 | config: 9 | import: optional:configtree:/run/secrets/ 10 | datasource: 11 | url: jdbc:postgresql://${POSTGRES_HOST:postgres}:${POSTGRES_PORT:5432}/${POSTGRES_DATABASE:bugzkit} 12 | password: ${postgres_password} 13 | jpa: 14 | hibernate: 15 | ddl-auto: create # change this to validate when liquibase is added 16 | data: 17 | redis: 18 | host: ${REDIS_HOST:redis} 19 | password: ${redis_password} 20 | security: 21 | user: 22 | password: ${user_password} # remove when liquibase is added 23 | oauth2: 24 | client: 25 | registration: 26 | google: 27 | client-id: ${google_client_id} 28 | client-secret: ${google_client_secret} 29 | scope: profile,email 30 | mail: 31 | password: ${smtp_password} 32 | 33 | jwt: 34 | secret: ${jwt_secret} 35 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | {@render children?.()} 27 | 28 | 29 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/user/payload/request/ChangePasswordRequest.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.payload.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import jakarta.validation.constraints.Pattern; 5 | import org.bugzkit.api.shared.constants.Regex; 6 | import org.bugzkit.api.shared.validator.FieldMatch; 7 | 8 | @FieldMatch( 9 | first = "newPassword", 10 | second = "confirmNewPassword", 11 | message = "{user.passwordsDoNotMatch}") 12 | public record ChangePasswordRequest( 13 | @NotBlank(message = "{user.passwordRequired}") 14 | @Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}") 15 | String currentPassword, 16 | @NotBlank(message = "{user.passwordRequired}") 17 | @Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}") 18 | String newPassword, 19 | @NotBlank(message = "{user.passwordRequired}") 20 | @Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}") 21 | String confirmNewPassword) {} 22 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/form/index.ts: -------------------------------------------------------------------------------- 1 | import * as FormPrimitive from 'formsnap'; 2 | import Description from './form-description.svelte'; 3 | import Label from './form-label.svelte'; 4 | import FieldErrors from './form-field-errors.svelte'; 5 | import Field from './form-field.svelte'; 6 | import Button from './form-button.svelte'; 7 | import Fieldset from './form-fieldset.svelte'; 8 | import Legend from './form-legend.svelte'; 9 | import ElementField from './form-element-field.svelte'; 10 | 11 | const Control = FormPrimitive.Control as typeof FormPrimitive.Control; 12 | 13 | export { 14 | Field, 15 | Control, 16 | Label, 17 | FieldErrors, 18 | Description, 19 | Fieldset, 20 | Legend, 21 | ElementField, 22 | Button, 23 | // 24 | Field as FormField, 25 | Control as FormControl, 26 | Description as FormDescription, 27 | Label as FormLabel, 28 | FieldErrors as FormFieldErrors, 29 | Fieldset as FormFieldset, 30 | Legend as FormLegend, 31 | ElementField as FormElementField, 32 | Button as FormButton, 33 | }; 34 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/sheet/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog as SheetPrimitive } from 'bits-ui'; 2 | 3 | import Overlay from './sheet-overlay.svelte'; 4 | import Content from './sheet-content.svelte'; 5 | import Header from './sheet-header.svelte'; 6 | import Footer from './sheet-footer.svelte'; 7 | import Title from './sheet-title.svelte'; 8 | import Description from './sheet-description.svelte'; 9 | 10 | const Root = SheetPrimitive.Root; 11 | const Close = SheetPrimitive.Close; 12 | const Trigger = SheetPrimitive.Trigger; 13 | const Portal = SheetPrimitive.Portal; 14 | 15 | export { 16 | Root, 17 | Close, 18 | Trigger, 19 | Portal, 20 | Overlay, 21 | Content, 22 | Header, 23 | Footer, 24 | Title, 25 | Description, 26 | // 27 | Root as Sheet, 28 | Close as SheetClose, 29 | Trigger as SheetTrigger, 30 | Portal as SheetPortal, 31 | Overlay as SheetOverlay, 32 | Content as SheetContent, 33 | Header as SheetHeader, 34 | Footer as SheetFooter, 35 | Title as SheetTitle, 36 | Description as SheetDescription, 37 | }; 38 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/admin/payload/request/PatchUserRequest.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.admin.payload.request; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.Pattern; 5 | import java.util.Set; 6 | import lombok.Builder; 7 | import org.bugzkit.api.shared.constants.Regex; 8 | import org.bugzkit.api.shared.validator.FieldMatch; 9 | import org.bugzkit.api.user.model.Role.RoleName; 10 | 11 | @Builder 12 | @FieldMatch(first = "password", second = "confirmPassword", message = "{user.passwordsDoNotMatch}") 13 | public record PatchUserRequest( 14 | @Pattern(regexp = Regex.USERNAME, message = "{user.usernameInvalid}") String username, 15 | @Email(message = "{user.emailInvalid}", regexp = Regex.EMAIL) String email, 16 | @Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}") String password, 17 | @Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}") String confirmPassword, 18 | Boolean active, 19 | Boolean lock, 20 | Set roleNames) {} 21 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "postbuild": "pagefind --site .next/server/app --output-path out/_pagefind", 9 | "start": "next start", 10 | "format": "prettier --write .", 11 | "lint": "prettier --check . && eslint .", 12 | "lint:fix": "prettier --write . && eslint --fix .", 13 | "check-updates": "npx npm-check-updates" 14 | }, 15 | "dependencies": { 16 | "next": "15.1.5", 17 | "nextra": "4.0.4", 18 | "nextra-theme-docs": "4.0.4", 19 | "react": "19.0.0", 20 | "react-dom": "19.0.0" 21 | }, 22 | "devDependencies": { 23 | "@eslint/eslintrc": "3.2.0", 24 | "@types/node": "22.10.7", 25 | "@types/react": "19.0.7", 26 | "@types/react-dom": "19.0.3", 27 | "eslint": "9.18.0", 28 | "eslint-config-next": "15.1.5", 29 | "eslint-config-prettier": "10.0.1", 30 | "pagefind": "1.3.0", 31 | "prettier": "3.4.2", 32 | "typescript": "5.7.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/oauth2/OAuth2UserPrincipal.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.oauth2; 2 | 3 | import java.util.Map; 4 | import org.bugzkit.api.auth.security.UserPrincipal; 5 | import org.springframework.security.oauth2.core.user.OAuth2User; 6 | 7 | public class OAuth2UserPrincipal extends UserPrincipal implements OAuth2User { 8 | private final Map attributes; 9 | 10 | public OAuth2UserPrincipal(UserPrincipal principal, Map attributes) { 11 | super( 12 | principal.getId(), 13 | principal.getUsername(), 14 | principal.getEmail(), 15 | principal.getPassword(), 16 | principal.isEnabled(), 17 | principal.isAccountNonLocked(), 18 | principal.getCreatedAt(), 19 | principal.getAuthorities()); 20 | this.attributes = attributes; 21 | } 22 | 23 | @Override 24 | public String getName() { 25 | return this.getEmail(); 26 | } 27 | 28 | @Override 29 | public Map getAttributes() { 30 | return this.attributes; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/logger/AspectLogger.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.logger; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.aspectj.lang.annotation.AfterReturning; 5 | import org.aspectj.lang.annotation.Aspect; 6 | import org.aspectj.lang.annotation.Before; 7 | import org.aspectj.lang.annotation.Pointcut; 8 | import org.springframework.stereotype.Component; 9 | 10 | @Slf4j 11 | @Aspect 12 | @Component 13 | public class AspectLogger { 14 | private final CustomLogger customLogger; 15 | 16 | public AspectLogger(CustomLogger customLogger) { 17 | log.info("AspectLogger Initialized"); 18 | this.customLogger = customLogger; 19 | } 20 | 21 | @Pointcut("@within(org.springframework.web.bind.annotation.RestController)") 22 | public void controllerLayer() {} 23 | 24 | @Before(value = "controllerLayer()") 25 | public void logBefore() { 26 | customLogger.info("Called"); 27 | } 28 | 29 | @AfterReturning(value = "controllerLayer()") 30 | public void logAfter() { 31 | customLogger.info("Finished"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/validator/impl/FieldMatchImpl.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.validator.impl; 2 | 3 | import jakarta.validation.ConstraintValidator; 4 | import jakarta.validation.ConstraintValidatorContext; 5 | import org.bugzkit.api.shared.validator.FieldMatch; 6 | import org.springframework.beans.BeanWrapperImpl; 7 | 8 | public class FieldMatchImpl implements ConstraintValidator { 9 | private String firstFieldName; 10 | private String secondFieldName; 11 | 12 | @Override 13 | public void initialize(FieldMatch constraint) { 14 | firstFieldName = constraint.first(); 15 | secondFieldName = constraint.second(); 16 | } 17 | 18 | public boolean isValid(Object obj, ConstraintValidatorContext context) { 19 | final var wrapper = new BeanWrapperImpl(obj); 20 | final var first = (String) wrapper.getPropertyValue(firstFieldName); 21 | final var second = (String) wrapper.getPropertyValue(secondFieldName); 22 | 23 | return first == null && second == null || first != null && first.equals(second); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/spring-boot/.run/IntegrationTests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/select/index.ts: -------------------------------------------------------------------------------- 1 | import { Select as SelectPrimitive } from 'bits-ui'; 2 | 3 | import GroupHeading from './select-group-heading.svelte'; 4 | import Item from './select-item.svelte'; 5 | import Content from './select-content.svelte'; 6 | import Trigger from './select-trigger.svelte'; 7 | import Separator from './select-separator.svelte'; 8 | import ScrollDownButton from './select-scroll-down-button.svelte'; 9 | import ScrollUpButton from './select-scroll-up-button.svelte'; 10 | 11 | const Root = SelectPrimitive.Root; 12 | const Group = SelectPrimitive.Group; 13 | 14 | export { 15 | Root, 16 | Item, 17 | Group, 18 | GroupHeading, 19 | Content, 20 | Trigger, 21 | Separator, 22 | ScrollDownButton, 23 | ScrollUpButton, 24 | // 25 | Root as Select, 26 | Item as SelectItem, 27 | Group as SelectGroup, 28 | GroupHeading as SelectGroupHeading, 29 | Content as SelectContent, 30 | Trigger as SelectTrigger, 31 | Separator as SelectSeparator, 32 | ScrollDownButton as SelectScrollDownButton, 33 | ScrollUpButton as SelectScrollUpButton, 34 | }; 35 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import type { Profile } from '$lib/models/user/user'; 2 | import { languageTag } from '$lib/paraglide/runtime'; 3 | import { makeRequest } from '$lib/server/apis/api'; 4 | import { HttpRequest, isAdmin, removeAuth } from '$lib/server/utils/util'; 5 | import { error, redirect } from '@sveltejs/kit'; 6 | import type { LayoutServerLoad } from './$types'; 7 | 8 | export const load = (async ({ locals, cookies, url }) => { 9 | if (!locals.userId) return { profile: null }; 10 | 11 | const response = await makeRequest( 12 | { 13 | method: HttpRequest.GET, 14 | path: `/profile`, 15 | }, 16 | cookies, 17 | ); 18 | 19 | if ('error' in response) { 20 | if (response.status == 401) removeAuth(cookies, locals); 21 | error(response.status, { message: response.error }); 22 | } 23 | 24 | const profile = response as Profile; 25 | if (!profile.username && url.pathname !== `/${languageTag()}/profile`) redirect(302, '/profile'); 26 | 27 | return { profile, isAdmin: isAdmin(cookies.get('accessToken')) }; 28 | }) satisfies LayoutServerLoad; 29 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/validator/FieldMatch.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.validator; 2 | 3 | import static java.lang.annotation.ElementType.ANNOTATION_TYPE; 4 | import static java.lang.annotation.ElementType.TYPE; 5 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 6 | 7 | import jakarta.validation.Constraint; 8 | import jakarta.validation.Payload; 9 | import java.lang.annotation.Documented; 10 | import java.lang.annotation.Retention; 11 | import java.lang.annotation.Target; 12 | import org.bugzkit.api.shared.validator.impl.FieldMatchImpl; 13 | 14 | @Target({TYPE, ANNOTATION_TYPE}) 15 | @Retention(RUNTIME) 16 | @Constraint(validatedBy = FieldMatchImpl.class) 17 | @Documented 18 | public @interface FieldMatch { 19 | String message() default ""; 20 | 21 | Class[] groups() default {}; 22 | 23 | Class[] payload() default {}; 24 | 25 | String first(); 26 | 27 | String second(); 28 | 29 | @Target({TYPE, ANNOTATION_TYPE}) 30 | @Retention(RUNTIME) 31 | @Documented 32 | @interface List { 33 | FieldMatch[] value(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/regex.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Between 2 and 16 characters long 3 | * It supports only letters, numbers, dot and underscore 4 | * Username can't start or end with dot or underscore 5 | * Username can't have two underscores or dots in a row 6 | * Username can't have underscore(_) and dot(.) in a row 7 | * 8 | * e.g. 9 | * test 10 | * test_test123 11 | * test.test123 12 | * test_test.test123 13 | * test123 14 | * 15 | * */ 16 | export const USERNAME_REGEX = /^(?=[a-zA-Z0-9._]{2,16}$)(?!.*[_.]{2})[^_.].*[^_.]$/; 17 | /* 18 | * Email Regex base on this article: 19 | * https://www.baeldung.com/java-email-validation-regex#regular-expression-by-rfc-5322-for-email-validation 20 | * */ 21 | export const EMAIL_REGEX = /^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$/; 22 | /* 23 | * At least one letter 24 | * At least one number 25 | * It can have a special characters 26 | * It can have an uppercase letter 27 | * It must be minimum 8 characters long 28 | * 29 | * e.g. 30 | * qwerty321 31 | * BlaBla123 32 | * blaBLA23"# 33 | * 34 | * */ 35 | export const PASSWORD_REGEX = /^(?=.*\d)(?=.*[a-z])(?=.*[a-zA-Z]).{8,40}$/; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dejan Zdravkovic 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 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | {PUBLIC_APP_NAME} 19 | 20 | 21 | 22 | {#if navigating.complete} 23 | 24 | {:else if data.profile} 25 | 26 | {@render children()} 27 | {:else} 28 | 29 | {@render children()} 30 | {/if} 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/config/WebMvcConfig.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.config; 2 | 3 | import org.bugzkit.api.shared.interceptor.RequestInterceptor; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 7 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 9 | 10 | @Configuration 11 | public class WebMvcConfig implements WebMvcConfigurer { 12 | @Value("${ui.url}") 13 | private String uiUrl; 14 | 15 | @Override 16 | public void addCorsMappings(CorsRegistry registry) { 17 | registry 18 | .addMapping("/**") 19 | .allowedOrigins(uiUrl) 20 | .allowedMethods("HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE") 21 | .allowCredentials(true) 22 | .maxAge(3600); 23 | } 24 | 25 | @Override 26 | public void addInterceptors(InterceptorRegistry registry) { 27 | registry.addInterceptor(new RequestInterceptor()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/svelte-kit/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import prettier from 'eslint-config-prettier'; 3 | import svelte from 'eslint-plugin-svelte'; 4 | import globals from 'globals'; 5 | import ts from 'typescript-eslint'; 6 | 7 | /** @type {import('eslint').Linter.Config[]} */ 8 | export default [ 9 | js.configs.recommended, 10 | ...ts.configs.recommended, 11 | ...svelte.configs['flat/recommended'], 12 | prettier, 13 | ...svelte.configs['flat/prettier'], 14 | { 15 | languageOptions: { 16 | globals: { 17 | ...globals.browser, 18 | ...globals.node, 19 | }, 20 | }, 21 | }, 22 | { 23 | files: ['**/*.svelte'], 24 | languageOptions: { 25 | parserOptions: { 26 | parser: ts.parser, 27 | }, 28 | }, 29 | }, 30 | { 31 | ignores: ['build/', '.svelte-kit/', 'dist/', 'src/lib/paraglide'], 32 | }, 33 | { 34 | rules: { 35 | '@typescript-eslint/no-unused-vars': [ 36 | 'error', 37 | { 38 | argsIgnorePattern: '^_', 39 | varsIgnorePattern: '^_', 40 | caughtErrorsIgnorePattern: '^_', 41 | }, 42 | ], 43 | }, 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/profile/settings/schema.ts: -------------------------------------------------------------------------------- 1 | import * as m from '$lib/paraglide/messages.js'; 2 | import { EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX } from '$lib/regex'; 3 | import { z, ZodIssueCode } from 'zod'; 4 | 5 | export const updateProfileSchema = z.object({ 6 | username: z.string().regex(USERNAME_REGEX, { message: m.profile_usernameInvalid() }), 7 | email: z.string().regex(EMAIL_REGEX, { message: m.profile_emailInvalid() }), 8 | }); 9 | 10 | export const changePasswordSchema = z 11 | .object({ 12 | currentPassword: z.string().regex(PASSWORD_REGEX, { message: m.profile_passwordInvalid() }), 13 | newPassword: z.string().regex(PASSWORD_REGEX, { message: m.profile_passwordInvalid() }), 14 | confirmNewPassword: z.string().regex(PASSWORD_REGEX, { message: m.profile_passwordInvalid() }), 15 | }) 16 | .superRefine(({ newPassword, confirmNewPassword }, ctx) => { 17 | if (newPassword !== confirmNewPassword) { 18 | ctx.addIssue({ 19 | code: ZodIssueCode.custom, 20 | path: ['confirmNewPassword'], 21 | message: m.profile_passwordsDoNotMatch(), 22 | }); 23 | } 24 | }); 25 | 26 | export const deleteSchema = z.object({}); 27 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/form/form-field.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | 24 | 25 | {#snippet children({ constraints, errors, tainted, value })} 26 |
27 | {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })} 28 |
29 | {/snippet} 30 |
31 | -------------------------------------------------------------------------------- /backend/spring-boot/src/test/java/org/bugzkit/api/shared/unit/MessageServiceTest.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.unit; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.mockito.Mockito.when; 5 | 6 | import org.bugzkit.api.shared.message.service.impl.MessageServiceImpl; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | import org.springframework.context.MessageSource; 13 | import org.springframework.context.i18n.LocaleContextHolder; 14 | 15 | @ExtendWith(MockitoExtension.class) 16 | class MessageServiceTest { 17 | @Mock private MessageSource messageSource; 18 | @InjectMocks private MessageServiceImpl messageService; 19 | 20 | @Test 21 | void getMessage() { 22 | when(messageSource.getMessage("{auth.unauthorized}", null, LocaleContextHolder.getLocale())) 23 | .thenReturn("API_ERROR_AUTH_UNAUTHORIZED"); 24 | final var actualMessage = messageService.getMessage("{auth.unauthorized}"); 25 | assertThat(actualMessage).isEqualTo("API_ERROR_AUTH_UNAUTHORIZED"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog as DialogPrimitive } from 'bits-ui'; 2 | 3 | import Title from './dialog-title.svelte'; 4 | import Footer from './dialog-footer.svelte'; 5 | import Header from './dialog-header.svelte'; 6 | import Overlay from './dialog-overlay.svelte'; 7 | import Content from './dialog-content.svelte'; 8 | import Description from './dialog-description.svelte'; 9 | 10 | const Root: typeof DialogPrimitive.Root = DialogPrimitive.Root; 11 | const Trigger: typeof DialogPrimitive.Trigger = DialogPrimitive.Trigger; 12 | const Close: typeof DialogPrimitive.Close = DialogPrimitive.Close; 13 | const Portal: typeof DialogPrimitive.Portal = DialogPrimitive.Portal; 14 | 15 | export { 16 | Root, 17 | Title, 18 | Portal, 19 | Footer, 20 | Header, 21 | Trigger, 22 | Overlay, 23 | Content, 24 | Description, 25 | Close, 26 | // 27 | Root as Dialog, 28 | Title as DialogTitle, 29 | Portal as DialogPortal, 30 | Footer as DialogFooter, 31 | Header as DialogHeader, 32 | Trigger as DialogTrigger, 33 | Overlay as DialogOverlay, 34 | Content as DialogContent, 35 | Description as DialogDescription, 36 | Close as DialogClose, 37 | }; 38 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | {#snippet children({ checked })} 23 | 24 | {#if checked} 25 | 26 | {/if} 27 | 28 | {@render childrenProp?.({ checked })} 29 | {/snippet} 30 | 31 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 27 | 28 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/form/form-element-field.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | 24 | 25 | {#snippet children({ constraints, errors, tainted, value })} 26 |
27 | {@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })} 28 |
29 | {/snippet} 30 |
31 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "app.name", 5 | "type": "java.lang.String", 6 | "description": "Application name." 7 | }, 8 | { 9 | "name": "ui.url", 10 | "type": "java.lang.String", 11 | "description": "UI application URL." 12 | }, 13 | { 14 | "name": "jwt.secret", 15 | "type": "java.lang.String", 16 | "description": "Secret used to encrypt JWT." 17 | }, 18 | { 19 | "name": "jwt.access-token.duration", 20 | "type": "java.lang.String", 21 | "description": "Access token duration time." 22 | }, 23 | { 24 | "name": "jwt.refresh-token.duration", 25 | "type": "java.lang.String", 26 | "description": "Refresh token duration time." 27 | }, 28 | { 29 | "name": "jwt.verify-email-token.duration", 30 | "type": "java.lang.String", 31 | "description": "Verify email token duration time." 32 | }, 33 | { 34 | "name": "jwt.reset-password-token.duration", 35 | "type": "java.lang.String", 36 | "description": "Reset password token duration time." 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /docs/src/app/layout.jsx: -------------------------------------------------------------------------------- 1 | import { Footer, Layout, Navbar } from 'nextra-theme-docs'; 2 | import 'nextra-theme-docs/style.css'; 3 | import { Head } from 'nextra/components'; 4 | import { getPageMap } from 'nextra/page-map'; 5 | 6 | // https://nextjs.org/docs/app/building-your-application/optimizing/metadata 7 | export const metadata = { 8 | title: { 9 | default: 'Docs | bugzkit', 10 | template: '%s | bugzkit', 11 | }, 12 | }; 13 | 14 | const navbar = bugzkit} projectLink="https://github.com/while1618/bugzkit" />; 15 | const footer =
{new Date().getFullYear()} © bugzkit.
; 16 | 17 | export default async function RootLayout({ children }) { 18 | return ( 19 | 20 | 21 | {/* https://github.com/vercel/next.js/discussions/72035 */} 22 | 23 | 29 | {children} 30 | 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/interceptor/RequestInterceptor.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.interceptor; 2 | 3 | import com.google.common.net.HttpHeaders; 4 | import jakarta.annotation.Nonnull; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import java.util.UUID; 8 | import org.slf4j.MDC; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.servlet.HandlerInterceptor; 11 | 12 | @Component 13 | public class RequestInterceptor implements HandlerInterceptor { 14 | @Override 15 | public boolean preHandle( 16 | @Nonnull HttpServletRequest request, 17 | @Nonnull HttpServletResponse response, 18 | @Nonnull Object handler) { 19 | final var requestId = UUID.randomUUID().toString(); 20 | response.setHeader(HttpHeaders.X_REQUEST_ID, requestId); 21 | MDC.put("REQUEST_ID", requestId); 22 | return true; 23 | } 24 | 25 | @Override 26 | public void afterCompletion( 27 | @Nonnull HttpServletRequest request, 28 | @Nonnull HttpServletResponse response, 29 | @Nonnull Object handler, 30 | Exception ex) { 31 | MDC.remove("REQUEST_ID"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/profile/settings/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 |
13 |
14 | 15 | 16 | 17 | {m.profile_personalInformation()} 18 | 19 | 20 | {m.profile_security()} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/auth/sign-in/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { apiErrors, makeRequest } from '$lib/server/apis/api'; 2 | import { HttpRequest } from '$lib/server/utils/util'; 3 | import { fail, redirect } from '@sveltejs/kit'; 4 | import { superValidate } from 'sveltekit-superforms'; 5 | import { zod } from 'sveltekit-superforms/adapters'; 6 | import type { Actions, PageServerLoad } from './$types'; 7 | import { signInSchema } from './schema'; 8 | 9 | export const load = (async ({ locals }) => { 10 | if (locals.userId) redirect(302, '/'); 11 | 12 | return { 13 | form: await superValidate(zod(signInSchema)), 14 | }; 15 | }) satisfies PageServerLoad; 16 | 17 | export const actions = { 18 | signIn: async ({ request, cookies }) => { 19 | const form = await superValidate(request, zod(signInSchema)); 20 | if (!form.valid) return fail(400, { form }); 21 | 22 | const response = await makeRequest( 23 | { 24 | method: HttpRequest.POST, 25 | path: '/auth/tokens', 26 | body: JSON.stringify(form.data), 27 | }, 28 | cookies, 29 | ); 30 | 31 | if ('error' in response) return apiErrors(response, form); 32 | 33 | redirect(302, '/'); 34 | }, 35 | } satisfies Actions; 36 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/payload/request/RegisterUserRequest.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.payload.request; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.Pattern; 6 | import lombok.Builder; 7 | import org.bugzkit.api.shared.constants.Regex; 8 | import org.bugzkit.api.shared.validator.FieldMatch; 9 | 10 | @Builder 11 | @FieldMatch(first = "password", second = "confirmPassword", message = "{user.passwordsDoNotMatch}") 12 | public record RegisterUserRequest( 13 | @NotBlank(message = "{user.usernameRequired}") 14 | @Pattern(regexp = Regex.USERNAME, message = "{user.usernameInvalid}") 15 | String username, 16 | @NotBlank(message = "{user.emailRequired}") 17 | @Email(message = "{user.emailInvalid}", regexp = Regex.EMAIL) 18 | String email, 19 | @NotBlank(message = "{user.passwordRequired}") 20 | @Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}") 21 | String password, 22 | @NotBlank(message = "{user.passwordRequired}") 23 | @Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}") 24 | String confirmPassword) {} 25 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/admin/user/schema.ts: -------------------------------------------------------------------------------- 1 | import * as m from '$lib/paraglide/messages.js'; 2 | import { EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX } from '$lib/regex'; 3 | import { z, ZodIssueCode } from 'zod'; 4 | 5 | export const createSchema = z 6 | .object({ 7 | username: z.string().regex(USERNAME_REGEX, { message: m.auth_usernameInvalid() }), 8 | email: z.string().regex(EMAIL_REGEX, { message: m.auth_emailInvalid() }), 9 | password: z.string().regex(PASSWORD_REGEX, { message: m.auth_passwordInvalid() }), 10 | confirmPassword: z.string().regex(PASSWORD_REGEX, { message: m.auth_passwordInvalid() }), 11 | active: z.boolean().default(true), 12 | lock: z.boolean().default(false), 13 | roleNames: z.string().array(), 14 | }) 15 | .superRefine(({ password, confirmPassword }, ctx) => { 16 | if (password !== confirmPassword) { 17 | ctx.addIssue({ 18 | code: ZodIssueCode.custom, 19 | path: ['confirmPassword'], 20 | message: m.auth_passwordsDoNotMatch(), 21 | }); 22 | } 23 | }); 24 | 25 | export const changeRolesSchema = z.object({ 26 | id: z.number(), 27 | roleNames: z.string().array(), 28 | }); 29 | 30 | export const actionSchema = z.object({ 31 | id: z.number(), 32 | }); 33 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/profile/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { apiErrors, makeRequest } from '$lib/server/apis/api'; 2 | import { HttpRequest } from '$lib/server/utils/util'; 3 | import { fail, redirect, type Actions } from '@sveltejs/kit'; 4 | import { superValidate } from 'sveltekit-superforms'; 5 | import { zod } from 'sveltekit-superforms/adapters'; 6 | import type { PageServerLoad } from './$types'; 7 | import { setUsernameSchema } from './schema'; 8 | 9 | export const load = (async ({ parent }) => { 10 | const { profile } = await parent(); 11 | const setUsernameForm = await superValidate(zod(setUsernameSchema)); 12 | return { setUsernameForm, profile }; 13 | }) satisfies PageServerLoad; 14 | 15 | export const actions = { 16 | setUsername: async ({ request, cookies }) => { 17 | const form = await superValidate(request, zod(setUsernameSchema)); 18 | if (!form.valid) return fail(400, { form }); 19 | 20 | const response = await makeRequest( 21 | { 22 | method: HttpRequest.PATCH, 23 | path: '/profile', 24 | body: JSON.stringify(form.data), 25 | }, 26 | cookies, 27 | ); 28 | 29 | if ('error' in response) return apiErrors(response, form); 30 | 31 | redirect(302, '/'); 32 | }, 33 | } satisfies Actions; 34 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/resources/templates/email/reset-password.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Password Reset Request

7 |

Hi ${name},

8 |

We received a request to reset the password for your ${appName} account. Please click the button below to reset your password.

9 |
10 | 11 | 12 | Reset Password 13 | 14 | 15 | 16 | If you didn’t request a password reset, please ignore this email. 17 | 18 |
19 |
20 | 21 | 22 | 23 | 24 |

© ${appName}. All rights reserved.

25 |
26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/select/select-item.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 25 | {#snippet children({ selected, highlighted })} 26 | 27 | {#if selected} 28 | 29 | {/if} 30 | 31 | {#if childrenProp} 32 | {@render childrenProp({ selected, highlighted })} 33 | {:else} 34 | {label || value} 35 | {/if} 36 | {/snippet} 37 | 38 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/alert-dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { AlertDialog as AlertDialogPrimitive } from 'bits-ui'; 2 | 3 | import Title from './alert-dialog-title.svelte'; 4 | import Action from './alert-dialog-action.svelte'; 5 | import Cancel from './alert-dialog-cancel.svelte'; 6 | import Footer from './alert-dialog-footer.svelte'; 7 | import Header from './alert-dialog-header.svelte'; 8 | import Overlay from './alert-dialog-overlay.svelte'; 9 | import Content from './alert-dialog-content.svelte'; 10 | import Description from './alert-dialog-description.svelte'; 11 | 12 | const Root = AlertDialogPrimitive.Root; 13 | const Trigger = AlertDialogPrimitive.Trigger; 14 | const Portal = AlertDialogPrimitive.Portal; 15 | 16 | export { 17 | Root, 18 | Title, 19 | Action, 20 | Cancel, 21 | Portal, 22 | Footer, 23 | Header, 24 | Trigger, 25 | Overlay, 26 | Content, 27 | Description, 28 | // 29 | Root as AlertDialog, 30 | Title as AlertDialogTitle, 31 | Action as AlertDialogAction, 32 | Cancel as AlertDialogCancel, 33 | Portal as AlertDialogPortal, 34 | Footer as AlertDialogFooter, 35 | Header as AlertDialogHeader, 36 | Trigger as AlertDialogTrigger, 37 | Overlay as AlertDialogOverlay, 38 | Content as AlertDialogContent, 39 | Description as AlertDialogDescription, 40 | }; 41 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/navbar/language-switcher.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | {labels[selectedLanguage]} 25 | 26 | 27 | {m.languages()} 28 | {#each availableLanguageTags as lang} 29 | 30 | {/each} 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 26 | 27 | -------------------------------------------------------------------------------- /docs/src/content/how-it-works/project-structure.mdx: -------------------------------------------------------------------------------- 1 | ## Project Structure 2 | 3 | ### Backend 4 | 5 | The backend follows a typical Spring Boot structure with a clear organization into the following main directories: 6 | 7 | - **auth**: Contains authentication and authorization logic. 8 | - **user**: Contains user related logic. 9 | - **admin**: Contains admin related logic - user management. 10 | - **shared**: Includes shared utilities, services, configs and components used across different modules. 11 | - **resources**: Standard Spring Boot resources directory for configuration files, static assets, and templates. 12 | 13 | The **test** directory mirrors this structure, ensuring tests are organized alongside the corresponding features. 14 | 15 | ### Frontend 16 | 17 | - **messages**: Stores internationalization (i18n) files for multilingual support. 18 | - **src/routes**: Files here represent the routes of the web application, aligning with the file-based routing paradigm of SvelteKit. 19 | - **src/lib**: This directory contains reusable logic and components, organized into: 20 | - **components**: UI components and building blocks. 21 | - **models**: Data models and interfaces. 22 | - **server**: Server-side utilities or APIs used by the frontend. 23 | - **utils**: Various utility functions available at the root of `lib` directory. 24 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/listener/OnSendJwtEmailListener.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.event.listener; 2 | 3 | import jakarta.mail.MessagingException; 4 | import java.io.IOException; 5 | import org.bugzkit.api.auth.jwt.event.OnSendJwtEmail; 6 | import org.bugzkit.api.auth.jwt.event.email.JwtEmailSupplier; 7 | import org.bugzkit.api.shared.email.service.EmailService; 8 | import org.springframework.context.ApplicationListener; 9 | import org.springframework.core.env.Environment; 10 | import org.springframework.stereotype.Component; 11 | 12 | @Component 13 | public class OnSendJwtEmailListener implements ApplicationListener { 14 | private final EmailService emailService; 15 | private final Environment environment; 16 | 17 | public OnSendJwtEmailListener(EmailService emailService, Environment environment) { 18 | this.emailService = emailService; 19 | this.environment = environment; 20 | } 21 | 22 | @Override 23 | public void onApplicationEvent(OnSendJwtEmail event) { 24 | try { 25 | new JwtEmailSupplier() 26 | .supplyEmail(event.getPurpose()) 27 | .sendEmail(emailService, environment, event.getUser(), event.getToken()); 28 | } catch (IOException | MessagingException e) { 29 | throw new RuntimeException(e); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/service/AuthService.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.service; 2 | 3 | import org.bugzkit.api.auth.payload.dto.AuthTokensDTO; 4 | import org.bugzkit.api.auth.payload.request.AuthTokensRequest; 5 | import org.bugzkit.api.auth.payload.request.ForgotPasswordRequest; 6 | import org.bugzkit.api.auth.payload.request.RegisterUserRequest; 7 | import org.bugzkit.api.auth.payload.request.ResetPasswordRequest; 8 | import org.bugzkit.api.auth.payload.request.VerificationEmailRequest; 9 | import org.bugzkit.api.auth.payload.request.VerifyEmailRequest; 10 | import org.bugzkit.api.user.payload.dto.UserDTO; 11 | 12 | public interface AuthService { 13 | UserDTO register(RegisterUserRequest registerUserRequest); 14 | 15 | AuthTokensDTO authenticate(AuthTokensRequest authTokensRequest, String ipAddress); 16 | 17 | void deleteTokens(String accessToken, String ipAddress); 18 | 19 | void deleteTokensOnAllDevices(); 20 | 21 | AuthTokensDTO refreshTokens(String refreshToken, String ipAddress); 22 | 23 | void forgotPassword(ForgotPasswordRequest forgotPasswordRequest); 24 | 25 | void resetPassword(ResetPasswordRequest resetPasswordRequest); 26 | 27 | void sendVerificationMail(VerificationEmailRequest verificationEmailRequest); 28 | 29 | void verifyEmail(VerifyEmailRequest verifyEmailRequest); 30 | } 31 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/resources/templates/email/verify-email.mjml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Welcome to ${appName}!

7 |

Hi ${name},

8 |

Thank you for signing up with ${appName}! We are excited to have you on board. To complete your registration and activate your account, please click the button below to verify your email address.

9 |
10 | 11 | 12 | Verify Your Email 13 | 14 | 15 | 16 | If you did not sign up for ${appName}, please ignore this email. 17 | 18 |
19 |
20 | 21 | 22 | 23 | 24 |

© ${appName}. All rights reserved.

25 |
26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/user/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.repository; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import org.bugzkit.api.user.model.User; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.jpa.repository.EntityGraph; 8 | import org.springframework.data.jpa.repository.JpaRepository; 9 | import org.springframework.data.jpa.repository.Query; 10 | 11 | public interface UserRepository extends JpaRepository { 12 | @Query("select u.id from User u order by u.id") 13 | List findAllUserIds(Pageable pageable); 14 | 15 | @EntityGraph(attributePaths = "roles") 16 | List findAllByIdIn(List ids); 17 | 18 | @EntityGraph(attributePaths = "roles") 19 | Optional findWithRolesById(Long id); 20 | 21 | Optional findByUsername(String username); 22 | 23 | @EntityGraph(attributePaths = "roles") 24 | Optional findWithRolesByUsername(String username); 25 | 26 | @EntityGraph(attributePaths = "roles") 27 | Optional findWithRolesByEmail(String email); 28 | 29 | Optional findByEmail(String email); 30 | 31 | Optional findByUsernameOrEmail(String username, String email); 32 | 33 | boolean existsByUsername(String username); 34 | 35 | boolean existsByEmail(String email); 36 | } 37 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/auth/forgot-password/+page.server.ts: -------------------------------------------------------------------------------- 1 | import * as m from '$lib/paraglide/messages.js'; 2 | import { apiErrors, makeRequest } from '$lib/server/apis/api'; 3 | import { HttpRequest } from '$lib/server/utils/util'; 4 | import { fail, redirect } from '@sveltejs/kit'; 5 | import { message, superValidate } from 'sveltekit-superforms'; 6 | import { zod } from 'sveltekit-superforms/adapters'; 7 | import type { Actions, PageServerLoad } from './$types'; 8 | import { forgotPasswordSchema } from './schema'; 9 | 10 | export const load = (async ({ locals }) => { 11 | if (locals.userId) redirect(302, '/'); 12 | 13 | return { 14 | form: await superValidate(zod(forgotPasswordSchema)), 15 | }; 16 | }) satisfies PageServerLoad; 17 | 18 | export const actions = { 19 | forgotPassword: async ({ request, cookies }) => { 20 | const form = await superValidate(request, zod(forgotPasswordSchema)); 21 | if (!form.valid) return fail(400, { form }); 22 | 23 | const response = await makeRequest( 24 | { 25 | method: HttpRequest.POST, 26 | path: '/auth/password/forgot', 27 | body: JSON.stringify(form.data), 28 | }, 29 | cookies, 30 | ); 31 | 32 | if ('error' in response) return apiErrors(response, form); 33 | 34 | return message(form, m.auth_forgotPasswordSuccess()); 35 | }, 36 | } satisfies Actions; 37 | -------------------------------------------------------------------------------- /docs/src/content/ci.mdx: -------------------------------------------------------------------------------- 1 | ## Continuous Integration (CI) 2 | 3 | ### Overview 4 | 5 | GitHub Actions is used for Continuous Integration (CI). You'll find the workflow files in the `.github/workflows` directory: 6 | 7 | - **`spring-boot.yml`**: Run linting, unit and integration tests for Spring Boot API. 8 | - **`svelte-kit.yml`**: Run linting, unit and integration tests for SvelteKit UI. 9 | - **`docs.yml`**: Run linting for the docs. 10 | 11 | The workflows are automatically triggered when changes are made to the corresponding directories. You can also trigger them manually via the GitHub Actions interface. 12 | 13 | ### Environment Variables 14 | 15 | The `svelte-kit.yml` workflow requires secrets and environment variables. 16 | 17 | Add the following as **variables** in the settings of your repo, under **Secrets and variables > Actions**: 18 | 19 | ```bash 20 | PUBLIC_APP_NAME=your_app_name 21 | PUBLIC_API_URL=http://localhost:8080 22 | ``` 23 | 24 | Add the following as a **secret** in the settings of your repo, under **Secrets and variables > Actions**: 25 | 26 | ```bash 27 | JWT_SECRET=secret 28 | ``` 29 | 30 | ### Running Workflows Manually 31 | 32 | To manually trigger any workflow: 33 | 34 | - Navigate to the **Actions** tab in your repository. 35 | - Select the desired workflow (e.g., `spring-boot.yml`, `svelte-kit.yml`, or `docs.yml`). 36 | - Click the **Run workflow** button. 37 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/security/UserDetailsServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.security; 2 | 3 | import org.bugzkit.api.shared.error.exception.ResourceNotFoundException; 4 | import org.bugzkit.api.user.repository.UserRepository; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.transaction.annotation.Transactional; 9 | 10 | @Service 11 | public class UserDetailsServiceImpl implements UserDetailsService { 12 | private final UserRepository userRepository; 13 | 14 | public UserDetailsServiceImpl(UserRepository userRepository) { 15 | this.userRepository = userRepository; 16 | } 17 | 18 | @Transactional 19 | public UserDetails loadUserByUserId(Long userId) { 20 | return userRepository 21 | .findById(userId) 22 | .map(UserPrincipal::create) 23 | .orElseThrow(() -> new ResourceNotFoundException("user.notFound")); 24 | } 25 | 26 | @Override 27 | @Transactional 28 | public UserDetails loadUserByUsername(String username) { 29 | return userRepository 30 | .findByUsernameOrEmail(username, username) 31 | .map(UserPrincipal::create) 32 | .orElseThrow(() -> new ResourceNotFoundException("user.notFound")); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/util/JwtUtil.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.util; 2 | 3 | import com.auth0.jwt.JWT; 4 | import com.auth0.jwt.algorithms.Algorithm; 5 | import java.time.Instant; 6 | import java.util.Set; 7 | import java.util.stream.Collectors; 8 | import org.bugzkit.api.user.payload.dto.RoleDTO; 9 | 10 | public class JwtUtil { 11 | private JwtUtil() {} 12 | 13 | public static void verify(String token, String secret, JwtPurpose purpose) { 14 | JWT.require(JwtUtil.getAlgorithm(secret)) 15 | .withClaim("purpose", purpose.name()) 16 | .build() 17 | .verify(token); 18 | } 19 | 20 | public static Algorithm getAlgorithm(String secret) { 21 | return Algorithm.HMAC512(secret.getBytes()); 22 | } 23 | 24 | public static Long getUserId(String token) { 25 | return Long.parseLong(JWT.decode(token).getIssuer()); 26 | } 27 | 28 | public static Set getRoleDTOs(String token) { 29 | final var roles = JWT.decode(token).getClaim("roles").asList(String.class); 30 | return roles.stream().map(RoleDTO::new).collect(Collectors.toSet()); 31 | } 32 | 33 | public static Instant getIssuedAt(String token) { 34 | return JWT.decode(token).getIssuedAtAsInstant(); 35 | } 36 | 37 | public enum JwtPurpose { 38 | ACCESS_TOKEN, 39 | REFRESH_TOKEN, 40 | VERIFY_EMAIL_TOKEN, 41 | RESET_PASSWORD_TOKEN 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/VerificationEmail.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.event.email; 2 | 3 | import com.google.common.io.CharStreams; 4 | import jakarta.mail.MessagingException; 5 | import java.io.IOException; 6 | import java.io.InputStreamReader; 7 | import java.nio.charset.StandardCharsets; 8 | import java.util.Objects; 9 | import org.bugzkit.api.shared.email.service.EmailService; 10 | import org.bugzkit.api.user.model.User; 11 | import org.springframework.core.env.Environment; 12 | import org.springframework.core.io.ClassPathResource; 13 | 14 | public class VerificationEmail implements JwtEmail { 15 | @Override 16 | public void sendEmail(EmailService emailService, Environment environment, User user, String token) 17 | throws IOException, MessagingException { 18 | final var template = 19 | new ClassPathResource("templates/email/verify-email.html").getInputStream(); 20 | final var link = environment.getProperty("ui.url") + "/auth/verify-email?token=" + token; 21 | final var body = 22 | CharStreams.toString(new InputStreamReader(template, StandardCharsets.UTF_8)) 23 | .replace("${name}", user.getUsername()) 24 | .replace("${link}", link) 25 | .replace("${appName}", Objects.requireNonNull(environment.getProperty("app.name"))); 26 | emailService.sendHtmlEmail(user.getEmail(), "Verify email", body); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/auth/jwt/event/email/ResetPasswordEmail.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.auth.jwt.event.email; 2 | 3 | import com.google.common.io.CharStreams; 4 | import jakarta.mail.MessagingException; 5 | import java.io.IOException; 6 | import java.io.InputStreamReader; 7 | import java.nio.charset.StandardCharsets; 8 | import java.util.Objects; 9 | import org.bugzkit.api.shared.email.service.EmailService; 10 | import org.bugzkit.api.user.model.User; 11 | import org.springframework.core.env.Environment; 12 | import org.springframework.core.io.ClassPathResource; 13 | 14 | public class ResetPasswordEmail implements JwtEmail { 15 | @Override 16 | public void sendEmail(EmailService emailService, Environment environment, User user, String token) 17 | throws IOException, MessagingException { 18 | final var template = 19 | new ClassPathResource("templates/email/reset-password.html").getInputStream(); 20 | final var link = environment.getProperty("ui.url") + "/auth/reset-password?token=" + token; 21 | final var body = 22 | CharStreams.toString(new InputStreamReader(template, StandardCharsets.UTF_8)) 23 | .replace("${name}", user.getUsername()) 24 | .replace("${link}", link) 25 | .replace("${appName}", Objects.requireNonNull(environment.getProperty("app.name"))); 26 | emailService.sendHtmlEmail(user.getEmail(), "Reset password", body); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/error/ErrorMessage.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.error; 2 | 3 | import com.google.gson.GsonBuilder; 4 | import com.google.gson.JsonPrimitive; 5 | import com.google.gson.JsonSerializer; 6 | import java.time.LocalDateTime; 7 | import java.time.format.DateTimeFormatter; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import lombok.Getter; 11 | import org.springframework.http.HttpStatus; 12 | 13 | @Getter 14 | public class ErrorMessage { 15 | private final LocalDateTime timestamp; 16 | private final int status; 17 | private final String error; 18 | private final List codes; 19 | 20 | public ErrorMessage(HttpStatus status) { 21 | this.timestamp = LocalDateTime.now(); 22 | this.status = status.value(); 23 | this.error = status.getReasonPhrase(); 24 | this.codes = new ArrayList<>(); 25 | } 26 | 27 | public void addCode(String code) { 28 | this.codes.add(code); 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return new GsonBuilder() 34 | .setPrettyPrinting() 35 | .registerTypeAdapter( 36 | LocalDateTime.class, 37 | (JsonSerializer) 38 | (localDateTime, type, jsonSerializationContext) -> 39 | new JsonPrimitive(localDateTime.format(DateTimeFormatter.ISO_DATE_TIME))) 40 | .create() 41 | .toJson(this); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/constants/Regex.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.constants; 2 | 3 | /* 4 | * Check for regex on: https://regexr.com/ 5 | * */ 6 | public class Regex { 7 | /* 8 | * Between 2 and 16 characters long 9 | * It supports only letters, numbers, dot and underscore 10 | * Username can't start or end with dot or underscore 11 | * Username can't have two underscores or dots in a row 12 | * Username can't have underscore(_) and dot(.) in a row 13 | * 14 | * e.g. 15 | * test 16 | * test_test123 17 | * test.test123 18 | * test_test.test123 19 | * test123 20 | * 21 | * */ 22 | public static final String USERNAME = "^(?=[a-zA-Z0-9._]{2,16}$)(?!.*[_.]{2})[^_.].*[^_.]$"; 23 | /* 24 | * Email Regex base on this article: 25 | * https://www.baeldung.com/java-email-validation-regex#regular-expression-by-rfc-5322-for-email-validation 26 | * */ 27 | public static final String EMAIL = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$"; 28 | /* 29 | * At least one letter 30 | * At least one number 31 | * It can have a special characters 32 | * It can have an uppercase letter 33 | * It must be minimum 8 characters long 34 | * 35 | * e.g. 36 | * qwerty321 37 | * BlaBla123 38 | * blaBLA23"# 39 | * 40 | * */ 41 | public static final String PASSWORD = "^(?=.*\\d)(?=.*[a-z])(?=.*[a-zA-Z]).{8,40}$"; 42 | 43 | private Regex() {} 44 | } 45 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | app: 2 | name: ${APP_NAME:bugzkit} 3 | server: 4 | port: ${API_PORT:8080} 5 | 6 | spring: 7 | sql: 8 | init: 9 | platform: postgres 10 | datasource: 11 | username: ${POSTGRES_USER:postgres} 12 | jpa: 13 | properties: 14 | jakarta: 15 | persistence: 16 | validation: 17 | mode: none 18 | hibernate: 19 | query: 20 | fail_on_pagination_over_collection_fetch: true 21 | open-in-view: false 22 | data: 23 | web: 24 | pageable: 25 | one-indexed-parameters: true 26 | default-page-size: 10 27 | redis: 28 | port: ${REDIS_PORT:6379} 29 | database: ${REDIS_DATABASE:0} 30 | timeout: 60 31 | mail: 32 | host: ${SMTP_HOST:smtp.sendgrid.net} 33 | port: ${SMTP_PORT:587} 34 | username: ${SMTP_USER:apikey} 35 | properties: 36 | sender: ${SMTP_SENDER:office@bugzkit.com} 37 | mail: 38 | smtp: 39 | auth: true 40 | starttls: 41 | enable: true 42 | 43 | jwt: 44 | access-token: 45 | duration: ${ACCESS_TOKEN_DURATION:900} 46 | refresh-token: 47 | duration: ${REFRESH_TOKEN_DURATION:604800} 48 | verify-email-token: 49 | duration: ${VERIFY_EMAIL_TOKEN_DURATION:900} 50 | reset-password-token: 51 | duration: ${RESET_PASSWORD_TOKEN_DURATION:900} 52 | 53 | springdoc: 54 | swagger-ui: 55 | url: /openapi.yml 56 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/checkbox/checkbox.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 26 | {#snippet children({ checked, indeterminate })} 27 | 28 | {#if indeterminate} 29 | 30 | {:else} 31 | 32 | {/if} 33 | 34 | {/snippet} 35 | 36 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/admin/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.admin.controller; 2 | 3 | import jakarta.validation.Valid; 4 | import org.bugzkit.api.admin.payload.request.PatchUserRequest; 5 | import org.bugzkit.api.admin.payload.request.UserRequest; 6 | import org.bugzkit.api.admin.service.UserService; 7 | import org.bugzkit.api.shared.constants.Path; 8 | import org.bugzkit.api.shared.generic.crud.CrudController; 9 | import org.bugzkit.api.user.payload.dto.UserDTO; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.annotation.PatchMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.RequestBody; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | @RestController("adminUserController") 18 | @RequestMapping(Path.ADMIN_USERS) 19 | public class UserController extends CrudController { 20 | private final UserService userService; 21 | 22 | public UserController(UserService userService) { 23 | super(userService); 24 | this.userService = userService; 25 | } 26 | 27 | @PatchMapping("/{id}") 28 | public ResponseEntity patch( 29 | @PathVariable("id") Long id, @Valid @RequestBody PatchUserRequest patchUserRequest) { 30 | return ResponseEntity.ok(userService.patch(id, patchUserRequest)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/user/payload/dto/UserDTO.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.payload.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.google.common.base.Objects; 7 | import com.google.gson.annotations.SerializedName; 8 | import java.time.LocalDateTime; 9 | import java.util.Set; 10 | import lombok.Builder; 11 | 12 | @Builder 13 | public record UserDTO( 14 | Long id, 15 | String username, 16 | String email, 17 | @JsonInclude(Include.NON_NULL) Boolean active, 18 | @JsonInclude(Include.NON_NULL) Boolean lock, 19 | LocalDateTime createdAt, 20 | @JsonInclude(Include.NON_NULL) @JsonProperty("roles") @SerializedName("roles") 21 | Set roleDTOs) { 22 | 23 | @Override 24 | public boolean equals(Object o) { 25 | if (this == o) return true; 26 | if (o == null || getClass() != o.getClass()) return false; 27 | UserDTO userDTO = (UserDTO) o; 28 | return active == userDTO.active 29 | && lock == userDTO.lock 30 | && Objects.equal(id, userDTO.id) 31 | && Objects.equal(username, userDTO.username) 32 | && Objects.equal(email, userDTO.email) 33 | && Objects.equal(roleDTOs, userDTO.roleDTOs); 34 | } 35 | 36 | @Override 37 | public int hashCode() { 38 | return Objects.hashCode(id, username, email, active, lock, roleDTOs); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/spring-boot/src/test/java/org/bugzkit/api/shared/util/IntegrationTestUtil.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.util; 2 | 3 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 4 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 5 | 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import org.bugzkit.api.auth.payload.dto.AuthTokensDTO; 8 | import org.bugzkit.api.auth.payload.request.AuthTokensRequest; 9 | import org.bugzkit.api.shared.constants.Path; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.test.web.servlet.MockMvc; 12 | 13 | public class IntegrationTestUtil { 14 | private IntegrationTestUtil() {} 15 | 16 | public static AuthTokensDTO authTokens( 17 | MockMvc mockMvc, ObjectMapper objectMapper, String username) throws Exception { 18 | final var authTokensRequest = new AuthTokensRequest(username, "qwerty123"); 19 | final var response = 20 | mockMvc 21 | .perform( 22 | post(Path.AUTH + "/tokens") 23 | .contentType(MediaType.APPLICATION_JSON) 24 | .content(objectMapper.writeValueAsString(authTokensRequest))) 25 | .andExpect(status().isNoContent()) 26 | .andReturn() 27 | .getResponse(); 28 | return new AuthTokensDTO( 29 | response.getCookie("accessToken").getValue(), 30 | response.getCookie("refreshToken").getValue()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/spring-boot/src/test/java/org/bugzkit/api/shared/unit/EmailServiceTest.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.unit; 2 | 3 | import static org.mockito.ArgumentMatchers.any; 4 | import static org.mockito.Mockito.times; 5 | import static org.mockito.Mockito.verify; 6 | import static org.mockito.Mockito.when; 7 | 8 | import jakarta.mail.MessagingException; 9 | import jakarta.mail.Session; 10 | import jakarta.mail.internet.MimeMessage; 11 | import java.io.UnsupportedEncodingException; 12 | import org.bugzkit.api.shared.email.service.impl.EmailServiceImpl; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.mockito.InjectMocks; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.jupiter.MockitoExtension; 18 | import org.springframework.mail.javamail.JavaMailSender; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | class EmailServiceTest { 22 | @Mock private JavaMailSender mailSender; 23 | @InjectMocks private EmailServiceImpl emailService; 24 | 25 | @Test 26 | void sendHtmlEmail() throws MessagingException, UnsupportedEncodingException { 27 | final var to = "user"; 28 | final var subject = "subject"; 29 | final var body = 30 | "Title

This is an html email.

"; 31 | when(mailSender.createMimeMessage()).thenReturn(new MimeMessage((Session) null)); 32 | emailService.sendHtmlEmail(to, subject, body); 33 | verify(mailSender, times(1)).send(any(MimeMessage.class)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/email/service/impl/EmailServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.email.service.impl; 2 | 3 | import jakarta.mail.MessagingException; 4 | import jakarta.mail.internet.InternetAddress; 5 | import java.io.UnsupportedEncodingException; 6 | import java.nio.charset.StandardCharsets; 7 | import org.bugzkit.api.shared.email.service.EmailService; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.mail.javamail.JavaMailSender; 10 | import org.springframework.mail.javamail.MimeMessageHelper; 11 | import org.springframework.stereotype.Service; 12 | 13 | @Service 14 | public class EmailServiceImpl implements EmailService { 15 | private final JavaMailSender mailSender; 16 | 17 | @Value("${app.name}") 18 | private String appName; 19 | 20 | @Value("${spring.mail.properties.sender}") 21 | private String sender; 22 | 23 | public EmailServiceImpl(JavaMailSender mailSender) { 24 | this.mailSender = mailSender; 25 | } 26 | 27 | @Override 28 | public void sendHtmlEmail(String to, String subject, String body) 29 | throws MessagingException, UnsupportedEncodingException { 30 | final var mimeMessage = mailSender.createMimeMessage(); 31 | final var helper = new MimeMessageHelper(mimeMessage, StandardCharsets.UTF_8.name()); 32 | helper.setFrom(new InternetAddress(sender, appName)); 33 | helper.setTo(to); 34 | helper.setSubject(subject); 35 | helper.setText(body, true); 36 | mailSender.send(mimeMessage); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | {#snippet children({ checked, indeterminate })} 31 | 32 | {#if indeterminate} 33 | 34 | {:else} 35 | 36 | {/if} 37 | 38 | {@render childrenProp?.()} 39 | {/snippet} 40 | 41 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/user/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.mapper; 2 | 3 | import java.util.Set; 4 | import org.bugzkit.api.auth.security.UserPrincipal; 5 | import org.bugzkit.api.user.model.Role; 6 | import org.bugzkit.api.user.model.User; 7 | import org.bugzkit.api.user.payload.dto.RoleDTO; 8 | import org.bugzkit.api.user.payload.dto.UserDTO; 9 | import org.mapstruct.InjectionStrategy; 10 | import org.mapstruct.Mapper; 11 | import org.mapstruct.Mapping; 12 | import org.mapstruct.factory.Mappers; 13 | 14 | @Mapper(injectionStrategy = InjectionStrategy.CONSTRUCTOR) 15 | public interface UserMapper { 16 | UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); 17 | 18 | Set rolesToRoleDTOs(Set roles); 19 | 20 | RoleDTO roleToRoleDTO(Role role); 21 | 22 | @Mapping(source = "roles", target = "roleDTOs") 23 | UserDTO userToAdminUserDTO(User user); 24 | 25 | @Mapping(target = "active", ignore = true) 26 | @Mapping(target = "lock", ignore = true) 27 | @Mapping(target = "roleDTOs", ignore = true) 28 | UserDTO userToProfileUserDTO(User user); 29 | 30 | @Mapping(target = "active", ignore = true) 31 | @Mapping(target = "lock", ignore = true) 32 | @Mapping(target = "roleDTOs", ignore = true) 33 | UserDTO userPrincipalToProfileUserDTO(UserPrincipal userPrincipal); 34 | 35 | @Mapping(target = "email", ignore = true) 36 | @Mapping(target = "active", ignore = true) 37 | @Mapping(target = "lock", ignore = true) 38 | @Mapping(target = "roleDTOs", ignore = true) 39 | UserDTO userToSimpleUserDTO(User user); 40 | } 41 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/admin/payload/request/UserRequest.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.admin.payload.request; 2 | 3 | import jakarta.validation.constraints.Email; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotEmpty; 6 | import jakarta.validation.constraints.NotNull; 7 | import jakarta.validation.constraints.Pattern; 8 | import java.util.Set; 9 | import lombok.Builder; 10 | import org.bugzkit.api.shared.constants.Regex; 11 | import org.bugzkit.api.shared.validator.FieldMatch; 12 | import org.bugzkit.api.user.model.Role.RoleName; 13 | 14 | @Builder 15 | @FieldMatch(first = "password", second = "confirmPassword", message = "{user.passwordsDoNotMatch}") 16 | public record UserRequest( 17 | @NotBlank(message = "{user.usernameRequired}") 18 | @Pattern(regexp = Regex.USERNAME, message = "{user.usernameInvalid}") 19 | String username, 20 | @NotBlank(message = "{user.emailRequired}") 21 | @Email(message = "{user.emailInvalid}", regexp = Regex.EMAIL) 22 | String email, 23 | @NotBlank(message = "{user.passwordRequired}") 24 | @Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}") 25 | String password, 26 | @NotBlank(message = "{user.passwordRequired}") 27 | @Pattern(regexp = Regex.PASSWORD, message = "{user.passwordInvalid}") 28 | String confirmPassword, 29 | @NotNull(message = "{user.activeRequired}") Boolean active, 30 | @NotNull(message = "{user.lockRequired}") Boolean lock, 31 | @NotEmpty(message = "{user.rolesEmpty}") Set roleNames) {} 32 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/shared/config/MailConfig.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.shared.config; 2 | 3 | import java.util.Objects; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.mail.javamail.JavaMailSender; 8 | import org.springframework.mail.javamail.JavaMailSenderImpl; 9 | 10 | @Configuration 11 | public class MailConfig { 12 | @Value("${spring.mail.host}") 13 | private String host; 14 | 15 | @Value("${spring.mail.port}") 16 | private String port; 17 | 18 | @Value("${spring.mail.username}") 19 | private String username; 20 | 21 | @Value("${spring.mail.password}") 22 | private String password; 23 | 24 | @Bean 25 | public JavaMailSender getJavaMailSender() { 26 | final var mailSender = createMailSender(); 27 | setProperties(mailSender); 28 | return mailSender; 29 | } 30 | 31 | private JavaMailSenderImpl createMailSender() { 32 | final var mailSender = new JavaMailSenderImpl(); 33 | mailSender.setHost(host); 34 | mailSender.setPort(Integer.parseInt(Objects.requireNonNull(port))); 35 | mailSender.setUsername(username); 36 | mailSender.setPassword(password); 37 | return mailSender; 38 | } 39 | 40 | private void setProperties(JavaMailSenderImpl mailSender) { 41 | final var props = mailSender.getJavaMailProperties(); 42 | props.put("mail.transport.protocol", "smtp"); 43 | props.put("mail.smtp.auth", "true"); 44 | props.put("mail.smtp.starttls.enable", "true"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/spring-boot/src/test/java/org/bugzkit/api/user/data/RoleRepositoryIT.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.data; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.Set; 6 | import org.bugzkit.api.shared.config.DatabaseContainers; 7 | import org.bugzkit.api.user.model.Role; 8 | import org.bugzkit.api.user.model.Role.RoleName; 9 | import org.bugzkit.api.user.repository.RoleRepository; 10 | import org.junit.jupiter.api.AfterEach; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 15 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 16 | import org.springframework.test.annotation.DirtiesContext; 17 | 18 | @DataJpaTest 19 | @DirtiesContext 20 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 21 | class RoleRepositoryIT extends DatabaseContainers { 22 | @Autowired private RoleRepository roleRepository; 23 | 24 | @BeforeEach 25 | void setUp() { 26 | roleRepository.save(new Role(RoleName.ADMIN)); 27 | roleRepository.save(new Role(RoleName.USER)); 28 | } 29 | 30 | @AfterEach 31 | void cleanUp() { 32 | roleRepository.deleteAll(); 33 | } 34 | 35 | @Test 36 | void findAllByNameIn() { 37 | final var roles = roleRepository.findAllByNameIn(Set.of(RoleName.USER, RoleName.ADMIN)); 38 | assertThat(roles).hasSize(2); 39 | } 40 | 41 | @Test 42 | void findByName() { 43 | assertThat(roleRepository.findByName(RoleName.USER)).isPresent(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/routes/auth/reset-password/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { env } from '$env/dynamic/private'; 2 | import * as m from '$lib/paraglide/messages.js'; 3 | import { apiErrors, makeRequest } from '$lib/server/apis/api'; 4 | import { HttpRequest } from '$lib/server/utils/util'; 5 | import { fail, redirect } from '@sveltejs/kit'; 6 | import jwt from 'jsonwebtoken'; 7 | import { setError, superValidate } from 'sveltekit-superforms'; 8 | import { zod } from 'sveltekit-superforms/adapters'; 9 | import type { Actions, PageServerLoad } from './$types'; 10 | import { resetPasswordSchema } from './schema'; 11 | 12 | export const load = (async ({ locals }) => { 13 | if (locals.userId) redirect(302, '/'); 14 | 15 | const form = await superValidate(zod(resetPasswordSchema)); 16 | return { form }; 17 | }) satisfies PageServerLoad; 18 | 19 | export const actions = { 20 | resetPassword: async ({ request, cookies, url }) => { 21 | const form = await superValidate(request, zod(resetPasswordSchema)); 22 | if (!form.valid) return fail(400, { form }); 23 | 24 | const token = url.searchParams.get('token') ?? ''; 25 | try { 26 | jwt.verify(token, env.JWT_SECRET); 27 | } catch (_) { 28 | return setError(form, m.auth_tokenInvalid()); 29 | } 30 | 31 | const response = await makeRequest( 32 | { 33 | method: HttpRequest.POST, 34 | path: '/auth/password/reset', 35 | body: JSON.stringify({ ...form.data, token }), 36 | }, 37 | cookies, 38 | ); 39 | 40 | if ('error' in response) return apiErrors(response, form); 41 | 42 | redirect(302, '/auth/sign-in'); 43 | }, 44 | } satisfies Actions; 45 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/resources/error-codes.properties: -------------------------------------------------------------------------------- 1 | auth.unauthorized=API_ERROR_AUTH_UNAUTHORIZED 2 | auth.forbidden=API_ERROR_AUTH_FORBIDDEN 3 | auth.tokenRequired=API_ERROR_AUTH_TOKEN_REQUIRED 4 | auth.tokenInvalid=API_ERROR_AUTH_TOKEN_INVALID 5 | 6 | user.usernameRequired=API_ERROR_USER_USERNAME_REQUIRED 7 | user.usernameInvalid=API_ERROR_USER_USERNAME_INVALID 8 | user.usernameExists=API_ERROR_USER_USERNAME_EXISTS 9 | user.usernameOrEmailRequired=API_ERROR_USER_USERNAME_OR_EMAIL_REQUIRED 10 | user.usernameOrEmailInvalid=API_ERROR_USER_USERNAME_OR_EMAIL_INVALID 11 | user.emailRequired=API_ERROR_USER_EMAIL_REQUIRED 12 | user.emailInvalid=API_ERROR_USER_EMAIL_INVALID 13 | user.emailExists=API_ERROR_USER_EMAIL_EXISTS 14 | user.passwordRequired=API_ERROR_USER_PASSWORD_REQUIRED 15 | user.passwordInvalid=API_ERROR_USER_PASSWORD_INVALID 16 | user.passwordsDoNotMatch=API_ERROR_USER_PASSWORDS_DO_NOT_MATCH 17 | user.currentPasswordWrong=API_ERROR_USER_CURRENT_PASSWORD_WRONG 18 | user.notFound=API_ERROR_USER_NOT_FOUND 19 | user.activeRequired=API_ERROR_USER_ACTIVE_REQUIRED 20 | user.active=API_ERROR_USER_ACTIVE 21 | user.notActive=API_ERROR_USER_NOT_ACTIVE 22 | user.lockRequired=API_ERROR_USER_LOCK_REQUIRED 23 | user.lock=API_ERROR_USER_LOCK 24 | user.rolesEmpty=API_ERROR_USER_ROLES_EMPTY 25 | user.roleNotFound=API_ERROR_USER_ROLE_NOT_FOUND 26 | 27 | request.parameterMissing=API_ERROR_REQUEST_PARAMETER_MISSING 28 | request.methodNotSupported=API_ERROR_REQUEST_METHOD_NOT_SUPPORTED 29 | request.messageNotReadable=API_ERROR_REQUEST_MESSAGE_NOT_READABLE 30 | request.parameterTypeMismatch=API_ERROR_REQUEST_PARAMETER_TYPE_MISMATCH 31 | 32 | server.internalError=API_ERROR_INTERNAL_SERVER_ERROR -------------------------------------------------------------------------------- /backend/spring-boot/.run/AllTests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 31 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/navbar/guest-navbar.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 |
13 |
14 |
15 | 16 | 17 | 18 | Toggle Menu 19 | 20 | 21 | 22 | {PUBLIC_APP_NAME} 23 | 24 |
25 |
26 |
27 |
28 |
29 | 30 | 31 | 32 |
33 |
34 | 35 |
36 | 37 | 41 |
42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /frontend/svelte-kit/src/lib/components/ui/select/select-content.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 28 | 29 | 34 | {@render children?.()} 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /backend/spring-boot/src/main/java/org/bugzkit/api/user/model/Role.java: -------------------------------------------------------------------------------- 1 | package org.bugzkit.api.user.model; 2 | 3 | import com.google.common.base.Objects; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.EnumType; 7 | import jakarta.persistence.Enumerated; 8 | import jakarta.persistence.GeneratedValue; 9 | import jakarta.persistence.GenerationType; 10 | import jakarta.persistence.Id; 11 | import jakarta.persistence.Index; 12 | import jakarta.persistence.Table; 13 | import java.io.Serial; 14 | import java.io.Serializable; 15 | import lombok.AllArgsConstructor; 16 | import lombok.Getter; 17 | import lombok.NoArgsConstructor; 18 | 19 | @Getter 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | @Entity 23 | @Table( 24 | name = "roles", 25 | indexes = @Index(name = "idx_role_name", columnList = "role_name", unique = true)) 26 | public class Role implements Serializable { 27 | @Serial private static final long serialVersionUID = 3717126169522609755L; 28 | 29 | @Id 30 | @GeneratedValue(strategy = GenerationType.IDENTITY) 31 | @Column(name = "role_id") 32 | private Long id; 33 | 34 | @Column(name = "role_name", unique = true, nullable = false) 35 | @Enumerated(EnumType.STRING) 36 | private RoleName name; 37 | 38 | public Role(RoleName name) { 39 | this.name = name; 40 | } 41 | 42 | @Override 43 | public boolean equals(Object o) { 44 | if (this == o) return true; 45 | if (o == null || getClass() != o.getClass()) return false; 46 | Role role = (Role) o; 47 | return Objects.equal(id, role.id) && name == role.name; 48 | } 49 | 50 | @Override 51 | public int hashCode() { 52 | return Objects.hashCode(id, name); 53 | } 54 | 55 | public enum RoleName { 56 | USER, 57 | ADMIN 58 | } 59 | } 60 | --------------------------------------------------------------------------------