├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── deploy.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── diff-generator.xml ├── discord.xml ├── icon.svg ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── jpb-settings.xml ├── kotlinc.xml ├── misc.xml ├── modules.xml ├── modules │ └── foxochat-backend.main.iml ├── sqldialects.xml └── vcs.xml ├── Dockerfile ├── LICENSE ├── README.md ├── build.gradle ├── docker-compose.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── su │ └── foxochat │ ├── Main.java │ ├── advice │ ├── ExceptionAdvice.java │ └── ValidationAdvice.java │ ├── config │ ├── APIConfig.java │ ├── AsyncConfig.java │ ├── EmailConfig.java │ ├── JwtConfig.java │ ├── MinioConfig.java │ ├── OpenAPIConfig.java │ ├── RestClientConfig.java │ ├── WebConfig.java │ └── WebSocketConfig.java │ ├── constant │ ├── APIConstant.java │ ├── AttachmentConstant.java │ ├── AttributeConstant.java │ ├── ChannelConstant.java │ ├── CloseCodeConstant.java │ ├── EmailConstant.java │ ├── ExceptionConstant.java │ ├── GatewayConstant.java │ ├── MemberConstant.java │ ├── OTPConstant.java │ ├── StorageConstant.java │ ├── TokenConstant.java │ ├── UserConstant.java │ └── ValidationConstant.java │ ├── controller │ ├── AuthenticationController.java │ ├── ChannelController.java │ ├── CommonController.java │ └── UserController.java │ ├── dto │ ├── api │ │ ├── request │ │ │ ├── AttachmentAddDTO.java │ │ │ ├── ChannelCreateDTO.java │ │ │ ├── ChannelEditDTO.java │ │ │ ├── MessageCreateDTO.java │ │ │ ├── OTPDTO.java │ │ │ ├── UserDeleteDTO.java │ │ │ ├── UserEditDTO.java │ │ │ ├── UserLoginDTO.java │ │ │ ├── UserRegisterDTO.java │ │ │ ├── UserResetPasswordConfirmDTO.java │ │ │ └── UserResetPasswordDTO.java │ │ └── response │ │ │ ├── AttachmentDTO.java │ │ │ ├── ChannelDTO.java │ │ │ ├── ExceptionDTO.java │ │ │ ├── InfoDTO.java │ │ │ ├── MemberDTO.java │ │ │ ├── MessageDTO.java │ │ │ ├── MessagesDTO.java │ │ │ ├── OkDTO.java │ │ │ ├── TokenDTO.java │ │ │ ├── UploadAttachmentDTO.java │ │ │ └── UserDTO.java │ ├── gateway │ │ ├── EventDTO.java │ │ ├── StatusDTO.java │ │ └── response │ │ │ ├── ExceptionDTO.java │ │ │ ├── HeartbeatACKDTO.java │ │ │ ├── HelloDTO.java │ │ │ └── TypingStartDTO.java │ └── internal │ │ └── AttachmentPresignedDTO.java │ ├── exception │ ├── BaseException.java │ ├── cdn │ │ ├── InvalidFileFormatException.java │ │ └── UploadFailedException.java │ ├── channel │ │ ├── ChannelAlreadyExistException.java │ │ └── ChannelNotFoundException.java │ ├── member │ │ ├── MemberAlreadyInChannelException.java │ │ ├── MemberInChannelNotFoundException.java │ │ └── MissingPermissionsException.java │ ├── message │ │ ├── AttachmentsCannotBeEmpty.java │ │ ├── MessageCannotBeEmpty.java │ │ ├── MessageNotFoundException.java │ │ └── UnknownAttachmentsException.java │ ├── otp │ │ ├── NeedToWaitBeforeResendException.java │ │ ├── OTPExpiredException.java │ │ └── OTPsInvalidException.java │ └── user │ │ ├── UserContactAlreadyExistException.java │ │ ├── UserContactNotFoundException.java │ │ ├── UserCredentialsDuplicateException.java │ │ ├── UserCredentialsIsInvalidException.java │ │ ├── UserEmailNotVerifiedException.java │ │ ├── UserNotFoundException.java │ │ └── UserUnauthorizedException.java │ ├── handler │ ├── HeartbeatHandler.java │ ├── HelloHandler.java │ ├── TypingStartHandler.java │ └── structure │ │ ├── BaseHandler.java │ │ ├── EventHandler.java │ │ └── EventHandlerRegistry.java │ ├── interceptor │ ├── AuthenticationInterceptor.java │ ├── ChannelInterceptor.java │ └── MemberInterceptor.java │ ├── model │ ├── Attachment.java │ ├── Channel.java │ ├── Member.java │ ├── Message.java │ ├── MessageAttachment.java │ ├── OTP.java │ ├── Session.java │ ├── User.java │ └── UserContact.java │ ├── repository │ ├── AttachmentRepository.java │ ├── ChannelRepository.java │ ├── MemberRepository.java │ ├── MessageRepository.java │ ├── OTPRepository.java │ └── UserRepository.java │ ├── service │ ├── AttachmentService.java │ ├── AuthenticationService.java │ ├── ChannelService.java │ ├── EmailService.java │ ├── GatewayService.java │ ├── JwtService.java │ ├── MemberService.java │ ├── MessageService.java │ ├── OTPService.java │ ├── StorageService.java │ ├── UserService.java │ └── impl │ │ ├── AttachmentServiceImpl.java │ │ ├── AuthenticationServiceImpl.java │ │ ├── ChannelServiceImpl.java │ │ ├── EmailServiceImpl.java │ │ ├── GatewayServiceImpl.java │ │ ├── JwtServiceImpl.java │ │ ├── MemberServiceImpl.java │ │ ├── MessageServiceImpl.java │ │ ├── OTPServiceImpl.java │ │ ├── StorageServiceImpl.java │ │ └── UserServiceImpl.java │ └── util │ ├── OTPGenerator.java │ ├── PasswordHasher.java │ └── StringUtils.java └── resources ├── application.example.yml ├── db └── migration │ ├── V10__.sql │ ├── V11__.sql │ ├── V12__.sql │ ├── V13__.sql │ ├── V1__.sql │ ├── V2__.sql │ ├── V3__.sql │ ├── V4__.sql │ ├── V5__.sql │ ├── V6__.sql │ ├── V7__.sql │ ├── V8__.sql │ └── V9__.sql └── templates └── email.html /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Backend 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | - dev 11 | 12 | jobs: 13 | build: 14 | runs-on: cloud 15 | environment: ${{ github.ref_name == 'main' && 'production' || github.ref_name == 'dev' && 'development' || 'development' }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Create config 20 | run: | 21 | cat < src/main/resources/application.properties 22 | spring.threads.virtual.enabled=true 23 | spring.jmx.enabled=false 24 | spring.datasource.driver-class-name=org.postgresql.Driver 25 | spring.datasource.username=${{ secrets.DB_USERNAME }} 26 | spring.datasource.password=${{ secrets.DB_PASSWORD }} 27 | spring.datasource.url=jdbc:postgresql://${{ secrets.DB_HOSTNAME }}:${{ secrets.DB_PORT }}/${{ secrets.DB_DATABASE }} 28 | spring.jpa.hibernate.ddl-auto=none 29 | spring.jpa.open-in-view=true 30 | spring.data.jpa.repositories.bootstrap-mode: deferred 31 | logging.level.root=${{ vars.ENV == 'dev' && 'INFO' || 'WARN' }} 32 | logging.level.org.springframework=${{ vars.ENV == 'dev' && 'INFO' || 'WARN' }} 33 | spring.jackson.property-naming-strategy=SNAKE_CASE 34 | springdoc.swagger-ui.enabled=false 35 | smtp.host=${{ secrets.SMTP_HOSTNAME }} 36 | smtp.port=${{ secrets.SMTP_PORT }} 37 | smtp.username=${{ secrets.SMTP_USERNAME }} 38 | smtp.password=${{ secrets.SMTP_PASSWORD }} 39 | smtp.email=${{ secrets.SMTP_EMAIL }} 40 | jwt.secret=${{ secrets.JWT_SECRET }} 41 | minio.url=${{ vars.MINIO_URL }} 42 | minio.name=${{ secrets.MINIO_NAME }} 43 | minio.secret=${{ secrets.MINIO_SECRET }} 44 | api.version=1 45 | api.env=${{ vars.ENV }} 46 | api.url=${{ vars.ENV == 'dev' && 'https://api-dev.foxochat.app' || 'https://api.foxochat.app' }} 47 | api.cdn.url=https://cdn.foxochat.app 48 | api.gateway.production_url=wss://api.foxochat.app 49 | api.gateway.development_url=wss://api-dev.foxochat.app 50 | api.app.production_url=https://app.foxochat.app 51 | api.app.development_url=https://app-dev.foxochat.app 52 | EOF 53 | 54 | - name: Build 55 | env: 56 | JAVA_HOME: ${{ vars.JAVA_HOME }} 57 | run: | 58 | export PATH="${{ vars.JAVA_HOME }}/bin:$PATH" 59 | ./gradlew build 60 | 61 | - name: Run 62 | env: 63 | SUFFIX: ${{ vars.ENV == 'dev' && '-dev' || '' }} 64 | run: docker compose up foxochat-backend$SUFFIX -d --build 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/**/workspace.xml 9 | .idea/**/tasks.xml 10 | .idea/**/usage.statistics.xml 11 | .idea/**/dictionaries 12 | .idea/**/shelf 13 | .idea/**/aws.xml 14 | .idea/**/contentModel.xml 15 | .idea/**/dataSources/ 16 | .idea/**/dataSources.xml 17 | .idea/**/dataSources.ids 18 | .idea/**/dataSources.local.xml 19 | .idea/**/sqlDataSources.xml 20 | .idea/**/dynamic.xml 21 | .idea/**/uiDesigner.xml 22 | .idea/**/dbnavigator.xml 23 | .idea/**/gradle.xml 24 | .idea/**/libraries 25 | .idea/**/mongoSettings.xml 26 | .idea/replstate.xml 27 | .idea/sonarlint/ 28 | .idea/httpRequests 29 | .idea/caches/build_file_checksums.ser 30 | *.iws 31 | 32 | ### Eclipse ### 33 | .apt_generated 34 | .classpath 35 | .factorypath 36 | .project 37 | .settings 38 | .springBeans 39 | .sts4-cache 40 | bin/ 41 | !**/src/main/**/bin/ 42 | !**/src/test/**/bin/ 43 | 44 | ### NetBeans ### 45 | /nbproject/private/ 46 | /nbbuild/ 47 | /dist/ 48 | /nbdist/ 49 | /.nb-gradle/ 50 | 51 | ### VS Code ### 52 | .vscode/ 53 | 54 | ### Mac OS ### 55 | .DS_Store 56 | 57 | ### Other ### 58 | application.yml 59 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | foxochat-backend -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/diff-generator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | -------------------------------------------------------------------------------- /.idea/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/jpb-settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/modules/foxochat-backend.main.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/sqldialects.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gcr.io/distroless/java21-debian12:latest 2 | COPY build/libs/foxochat-backend-1.0.0.jar ./foxochat-backend.jar 3 | CMD ["foxochat-backend.jar"] 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 FoxoСorp 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FoxoChat Backend 2 | Backend service of FoxoChat 3 | ## Resources 4 | 5 | - **Website** 6 | - https://foxochat.app 7 | - **Discord** 8 | - https://discord.foxochat.app 9 | - **GitHub** 10 | - https://github.com/foxocorp 11 | 12 | ## Acknowledgements 13 | 14 | We would like to thank the following people and organizations for their valuable input and support in the development of the backend: 15 | 16 | - **Java** for making it possible for this project to exist 17 | - **Foxes** they are just so cute :3 18 | 19 | Without your help, the **FoxoChat Backend** project would not have been possible. We are grateful for your participation 20 | and support! 21 | 22 | ## License 23 | 24 | This project is licensed under the MIT license - see [LICENSE](LICENSE) for details. 25 | 26 | If you have any questions or problems with **FoxoChat Backend**, please contact us 27 | at [Discord](https://discord.foxochat.su). 28 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.5.0' 4 | id 'io.spring.dependency-management' version '1.1.7' 5 | } 6 | 7 | group = 'su.foxochat' 8 | version = '1.0.0' 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | configurations.configureEach { 15 | exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' 16 | } 17 | 18 | dependencies { 19 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 20 | implementation 'org.springframework.boot:spring-boot-starter-validation' 21 | implementation 'org.springframework.boot:spring-boot-starter-mail' 22 | implementation 'org.springframework.boot:spring-boot-starter-websocket' 23 | implementation 'org.springframework.boot:spring-boot-starter-web' 24 | implementation 'org.springframework.boot:spring-boot-starter-undertow' 25 | implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8' 26 | implementation 'org.mindrot:jbcrypt:0.4' 27 | implementation 'io.minio:minio:8.5.17' 28 | implementation 'org.flywaydb:flyway-core' 29 | implementation 'org.flywaydb:flyway-database-postgresql' 30 | runtimeOnly 'org.postgresql:postgresql' 31 | implementation 'io.jsonwebtoken:jjwt-api:0.12.6' 32 | runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' 33 | runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' 34 | compileOnly 'org.projectlombok:lombok' 35 | annotationProcessor 'org.projectlombok:lombok' 36 | } 37 | 38 | jar { 39 | manifest { 40 | attributes( 41 | 'Main-Class': 'su.foxochat.Main' 42 | ) 43 | } 44 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 45 | from { 46 | configurations.runtimeClasspath.collect { 47 | it.isDirectory() ? it : zipTree(it) 48 | } 49 | } 50 | } 51 | 52 | tasks.named('bootRun') { 53 | doFirst { 54 | jvmArgs = ["-Dspring.output.ansi.enabled=ALWAYS", "-XX:+UseShenandoahGC"] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | x-common-settings: &common-settings 2 | restart: always 3 | build: 4 | context: . 5 | networks: 6 | - db_network 7 | - minio_network 8 | - traefik 9 | 10 | services: 11 | foxochat-backend: 12 | <<: *common-settings 13 | container_name: foxochat-backend 14 | image: foxochat/backend:prod 15 | labels: 16 | traefik.enable: true 17 | traefik.http.routers.foxochat-backend.rule: Host(`api.foxochat.app`) 18 | traefik.http.services.foxochat-backend.loadbalancer.server.port: 8080 19 | traefik.http.routers.foxochat-backend.middlewares: ratelimit@file 20 | 21 | foxochat-backend-dev: 22 | <<: *common-settings 23 | container_name: foxochat-backend-dev 24 | image: foxochat/backend:dev 25 | labels: 26 | traefik.enable: true 27 | traefik.http.routers.foxochat-backend-dev.rule: Host(`api-dev.foxochat.app`) 28 | traefik.http.services.foxochat-backend-dev.loadbalancer.server.port: 8080 29 | traefik.http.routers.foxochat-backend-dev.middlewares: ratelimit@file 30 | 31 | networks: 32 | db_network: 33 | external: true 34 | minio_network: 35 | external: true 36 | traefik: 37 | external: true 38 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel=true 2 | org.gradle.daemon=true 3 | org.gradle.caching=true 4 | org.gradle.configuration-cache=true 5 | org.gradle.jvmargs=-Xmx2048M 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foxocorp/foxogram-backend/e9c66ecadd08e5bb9a0c33f1180be508b591a4f8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'foxochat-backend' 2 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/Main.java: -------------------------------------------------------------------------------- 1 | package su.foxochat; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Main { 8 | public static void main(String[] args) { 9 | SpringApplication.run(Main.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/advice/ExceptionAdvice.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.advice; 2 | 3 | import jakarta.validation.ConstraintViolation; 4 | import jakarta.validation.ConstraintViolationException; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.context.support.DefaultMessageSourceResolvable; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.http.converter.HttpMessageNotReadableException; 10 | import org.springframework.web.bind.MethodArgumentNotValidException; 11 | import org.springframework.web.bind.annotation.ExceptionHandler; 12 | import org.springframework.web.bind.annotation.RestControllerAdvice; 13 | import org.springframework.web.servlet.NoHandlerFoundException; 14 | import su.foxochat.config.APIConfig; 15 | import su.foxochat.constant.ExceptionConstant; 16 | import su.foxochat.dto.api.response.ExceptionDTO; 17 | import su.foxochat.exception.BaseException; 18 | 19 | import java.util.stream.Collectors; 20 | 21 | @Slf4j 22 | @RestControllerAdvice 23 | public class ExceptionAdvice { 24 | 25 | private final APIConfig apiConfig; 26 | 27 | public ExceptionAdvice(APIConfig apiConfig) { 28 | this.apiConfig = apiConfig; 29 | } 30 | 31 | private ResponseEntity buildErrorResponse(int errorCode, String message, HttpStatus status) { 32 | log.error(ExceptionConstant.Messages.SERVER_EXCEPTION.getValue(), errorCode, status, message); 33 | return ResponseEntity.status(status).body(new ExceptionDTO(false, errorCode, message)); 34 | } 35 | 36 | @ExceptionHandler(BaseException.class) 37 | public ResponseEntity handleBaseException(BaseException exception) { 38 | return buildErrorResponse(exception.getErrorCode(), exception.getMessage(), exception.getStatus()); 39 | } 40 | 41 | @ExceptionHandler(HttpMessageNotReadableException.class) 42 | public ResponseEntity handleHttpMessageNotReadable() { 43 | return buildErrorResponse(ExceptionConstant.API.EMPTY_BODY.getValue(), ExceptionConstant.Messages.REQUEST_BODY_EMPTY.getValue(), HttpStatus.BAD_REQUEST); 44 | } 45 | 46 | @ExceptionHandler(MethodArgumentNotValidException.class) 47 | public ResponseEntity handleValidationException(MethodArgumentNotValidException exception) { 48 | String message = exception.getBindingResult().getAllErrors() 49 | .stream() 50 | .map(DefaultMessageSourceResolvable::getDefaultMessage) 51 | .collect(Collectors.joining(", ")); 52 | 53 | return buildErrorResponse(ExceptionConstant.API.VALIDATION_ERROR.getValue(), message, HttpStatus.BAD_REQUEST); 54 | } 55 | 56 | @ExceptionHandler(ConstraintViolationException.class) 57 | public ResponseEntity handleConstraintViolationException(ConstraintViolationException exception) { 58 | String message = exception.getConstraintViolations() 59 | .stream() 60 | .map(ConstraintViolation::getMessage) 61 | .collect(Collectors.joining(", ")); 62 | 63 | return buildErrorResponse(ExceptionConstant.API.VALIDATION_ERROR.getValue(), message, HttpStatus.BAD_REQUEST); 64 | } 65 | 66 | @ExceptionHandler(NoHandlerFoundException.class) 67 | public ResponseEntity handleNoHandlerFoundException() { 68 | return buildErrorResponse(ExceptionConstant.API.ROUTE_NOT_FOUND.getValue(), ExceptionConstant.Messages.ROUTE_NOT_FOUND.getValue(), HttpStatus.NOT_FOUND); 69 | } 70 | 71 | @ExceptionHandler(Exception.class) 72 | public ResponseEntity handleException(Exception exception) { 73 | String message = exception.getMessage(); 74 | if (!apiConfig.isDevelopment()) message = ExceptionConstant.Messages.INTERNAL_ERROR.getValue(); 75 | 76 | log.error(ExceptionConstant.Messages.SERVER_EXCEPTION_STACKTRACE.getValue(), exception); 77 | return buildErrorResponse(ExceptionConstant.Unknown.ERROR.getValue(), message, HttpStatus.INTERNAL_SERVER_ERROR); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/advice/ValidationAdvice.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.advice; 2 | 3 | import jakarta.validation.ConstraintViolation; 4 | import jakarta.validation.ConstraintViolationException; 5 | import jakarta.validation.Validator; 6 | import org.springframework.core.MethodParameter; 7 | import org.springframework.http.HttpInputMessage; 8 | import org.springframework.http.converter.HttpMessageConverter; 9 | import org.springframework.lang.NonNull; 10 | import org.springframework.web.bind.annotation.RestControllerAdvice; 11 | import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice; 12 | 13 | import java.lang.reflect.Type; 14 | import java.util.Set; 15 | 16 | @RestControllerAdvice 17 | public class ValidationAdvice implements RequestBodyAdvice { 18 | private final Validator validator; 19 | 20 | public ValidationAdvice(Validator validator) { 21 | this.validator = validator; 22 | } 23 | 24 | @Override 25 | public boolean supports(@NonNull MethodParameter methodParameter, @NonNull Type targetType, @NonNull Class> converterType) { 26 | return true; 27 | } 28 | 29 | @Override 30 | @NonNull 31 | public HttpInputMessage beforeBodyRead(@NonNull HttpInputMessage inputMessage, @NonNull MethodParameter parameter, @NonNull Type targetType, @NonNull Class> converterType) { 32 | return inputMessage; 33 | } 34 | 35 | @Override 36 | @NonNull 37 | public Object afterBodyRead(@NonNull Object body, @NonNull HttpInputMessage inputMessage, @NonNull MethodParameter parameter, @NonNull Type targetType, @NonNull Class> converterType) { 38 | Set> violations = validator.validate(body); 39 | if (!violations.isEmpty()) { 40 | throw new ConstraintViolationException(violations); 41 | } 42 | return body; 43 | } 44 | 45 | @Override 46 | public Object handleEmptyBody(Object body, @NonNull HttpInputMessage inputMessage, @NonNull MethodParameter parameter, @NonNull Type targetType, @NonNull Class> converterType) { 47 | return body; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/config/APIConfig.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.config; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | @Configuration 11 | @ConfigurationProperties("api") 12 | @Getter 13 | @Setter 14 | public class APIConfig { 15 | 16 | private String version; 17 | 18 | private String env; 19 | 20 | private String url; 21 | 22 | @Value("${api.cdn.url}") 23 | private String cdnURL; 24 | 25 | @Value("${api.gateway.production_url}") 26 | private String gatewayURL; 27 | 28 | @Value("${api.gateway.development_url}") 29 | private String devGatewayURL; 30 | 31 | @Value("${api.app.production_url}") 32 | private String appURL; 33 | 34 | @Value("${api.app.development_url}") 35 | private String devAppURL; 36 | 37 | @Bean 38 | public boolean isDevelopment() { 39 | return env.equals("dev"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/config/AsyncConfig.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.core.task.TaskExecutor; 6 | import org.springframework.scheduling.annotation.EnableAsync; 7 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 8 | 9 | @Configuration 10 | @EnableAsync 11 | public class AsyncConfig { 12 | 13 | @Bean 14 | public TaskExecutor taskExecutor() { 15 | return new ThreadPoolTaskExecutor(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/config/EmailConfig.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.config; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.mail.javamail.JavaMailSender; 9 | import org.springframework.mail.javamail.JavaMailSenderImpl; 10 | 11 | import java.util.Properties; 12 | 13 | @Configuration 14 | @ConfigurationProperties("smtp") 15 | @Getter 16 | @Setter 17 | public class EmailConfig { 18 | 19 | private String host; 20 | 21 | private String port; 22 | 23 | private String email; 24 | 25 | private String username; 26 | 27 | private String password; 28 | 29 | @Bean 30 | public JavaMailSender javaMailSender() { 31 | JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); 32 | mailSender.setHost(host); 33 | mailSender.setPort(Integer.parseInt(port)); 34 | 35 | mailSender.setUsername(username); 36 | mailSender.setPassword(password); 37 | 38 | Properties props = mailSender.getJavaMailProperties(); 39 | props.put("mail.transport.protocol", "smtp"); 40 | props.put("mail.smtp.auth", "true"); 41 | props.put("mail.smtp.starttls.enable", "true"); 42 | props.put("mail.debug", "false"); 43 | 44 | return mailSender; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/config/JwtConfig.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.config; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | @ConfigurationProperties("jwt") 10 | @Getter 11 | @Setter 12 | public class JwtConfig { 13 | 14 | private String secret; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/config/MinioConfig.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.config; 2 | 3 | import io.minio.MinioAsyncClient; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | @ConfigurationProperties("minio") 13 | @Setter 14 | @Getter 15 | public class MinioConfig { 16 | 17 | @Value("${minio.url}") 18 | private String url; 19 | 20 | @Value("${minio.name}") 21 | private String accessKey; 22 | 23 | @Value("${minio.secret}") 24 | private String accessSecret; 25 | 26 | @Bean 27 | public MinioAsyncClient minioClient() { 28 | return MinioAsyncClient.builder() 29 | .endpoint(url) 30 | .credentials(accessKey, accessSecret) 31 | .build(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/config/OpenAPIConfig.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.config; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.PropertyNamingStrategies; 6 | import io.swagger.v3.core.converter.ModelConverters; 7 | import io.swagger.v3.core.jackson.ModelResolver; 8 | import io.swagger.v3.oas.models.Components; 9 | import io.swagger.v3.oas.models.OpenAPI; 10 | import io.swagger.v3.oas.models.info.Info; 11 | import io.swagger.v3.oas.models.security.SecurityRequirement; 12 | import io.swagger.v3.oas.models.security.SecurityScheme; 13 | import io.swagger.v3.oas.models.servers.Server; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.context.annotation.Configuration; 16 | 17 | import java.util.Arrays; 18 | import java.util.Collections; 19 | import java.util.List; 20 | 21 | @Configuration 22 | public class OpenAPIConfig { 23 | 24 | private final APIConfig apiConfig; 25 | 26 | public OpenAPIConfig(APIConfig apiConfig) { 27 | this.apiConfig = apiConfig; 28 | } 29 | 30 | @Bean 31 | public OpenAPI openAPI() { 32 | Info info = new Info() 33 | .title("FoxoChat") 34 | .version(apiConfig.getVersion()); 35 | 36 | List servers = Arrays.asList( 37 | new Server().url("https://foxochat.su").description("Production"), 38 | new Server().url("https://dev.foxochat.su").description("Development") 39 | ); 40 | 41 | if (apiConfig.isDevelopment()) { 42 | Collections.reverse(servers); 43 | } 44 | 45 | // Enable bearer authorization 46 | SecurityRequirement securityRequirement = new SecurityRequirement().addList("Authorization"); 47 | 48 | Components components = new Components() 49 | .addSecuritySchemes("Authorization", new SecurityScheme() 50 | .type(SecurityScheme.Type.HTTP) 51 | .scheme("bearer") 52 | .bearerFormat("JWT")); 53 | 54 | // Disable constructor fields and set snake_case 55 | ObjectMapper objectMapper = new ObjectMapper(); 56 | 57 | objectMapper.setVisibility(objectMapper.getSerializationConfig().getDefaultVisibilityChecker() 58 | .withCreatorVisibility(JsonAutoDetect.Visibility.NONE)) 59 | .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); 60 | 61 | ModelConverters.getInstance().addConverter(new ModelResolver(objectMapper)); 62 | 63 | return new OpenAPI() 64 | .info(info) 65 | .servers(servers) 66 | .addSecurityItem(securityRequirement) 67 | .components(components); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/config/RestClientConfig.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.http.client.JdkClientHttpRequestFactory; 8 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 9 | import org.springframework.web.client.RestClient; 10 | 11 | import java.net.http.HttpClient; 12 | import java.time.Duration; 13 | import java.util.List; 14 | 15 | @Slf4j 16 | @Configuration 17 | public class RestClientConfig { 18 | 19 | private final APIConfig apiConfig; 20 | 21 | private final ObjectMapper objectMapper; 22 | 23 | public RestClientConfig(APIConfig apiConfig, ObjectMapper objectMapper) { 24 | this.apiConfig = apiConfig; 25 | this.objectMapper = objectMapper; 26 | } 27 | 28 | @Bean 29 | public RestClient restClient() { 30 | HttpClient httpClient = HttpClient.newBuilder() 31 | .connectTimeout(Duration.ofMillis(1000)) 32 | .build(); 33 | 34 | return RestClient.builder() 35 | .requestFactory(new JdkClientHttpRequestFactory(httpClient)) 36 | .messageConverters(List.of(new MappingJackson2HttpMessageConverter(objectMapper))) 37 | .baseUrl(apiConfig.getUrl()) 38 | .build(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 5 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | import su.foxochat.interceptor.AuthenticationInterceptor; 8 | import su.foxochat.interceptor.ChannelInterceptor; 9 | import su.foxochat.interceptor.MemberInterceptor; 10 | 11 | @Configuration 12 | public class WebConfig implements WebMvcConfigurer { 13 | 14 | private final AuthenticationInterceptor authenticationInterceptor; 15 | 16 | private final ChannelInterceptor channelInterceptor; 17 | 18 | private final MemberInterceptor memberInterceptor; 19 | 20 | public WebConfig(AuthenticationInterceptor authenticationInterceptor, ChannelInterceptor channelInterceptor, MemberInterceptor memberInterceptor) { 21 | this.authenticationInterceptor = authenticationInterceptor; 22 | this.channelInterceptor = channelInterceptor; 23 | this.memberInterceptor = memberInterceptor; 24 | } 25 | 26 | @Override 27 | public void addCorsMappings(CorsRegistry registry) { 28 | registry.addMapping("/**") 29 | .allowedOrigins("*") 30 | .allowedMethods("*") 31 | .allowedHeaders("*"); 32 | } 33 | 34 | @Override 35 | public void addInterceptors(InterceptorRegistry registry) { 36 | registry.addInterceptor(authenticationInterceptor).excludePathPatterns("/info", "/auth/register", "/auth/login", "/auth/reset-password", "/auth/reset-password/**", "/swagger-ui/**", "/v3/api-docs/**", "/actuator/health"); 37 | registry.addInterceptor(channelInterceptor).excludePathPatterns("/info", "/auth/**", "/users/**", "/channels/", "/channels/@**", "/swagger-ui/**", "/v3/api-docs/**", "/actuator/health"); 38 | registry.addInterceptor(memberInterceptor).excludePathPatterns("/info", "/auth/**", "/users/**", "/channels/", "/channels/@**", "/swagger-ui/**", "/v3/api-docs/**", "/actuator/health"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.lang.NonNull; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocket; 6 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 7 | import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 8 | import su.foxochat.handler.structure.EventHandler; 9 | 10 | @Configuration 11 | @EnableWebSocket 12 | public class WebSocketConfig implements WebSocketConfigurer { 13 | 14 | private final EventHandler eventHandler; 15 | 16 | public WebSocketConfig(EventHandler eventHandler) { 17 | this.eventHandler = eventHandler; 18 | } 19 | 20 | @Override 21 | public void registerWebSocketHandlers(@NonNull WebSocketHandlerRegistry registry) { 22 | registry.addHandler(eventHandler, "/") 23 | .setAllowedOrigins("*"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/constant/APIConstant.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.constant; 2 | 3 | public class APIConstant { 4 | 5 | public static final String USERS = "/users"; 6 | 7 | public static final String AUTH = "/auth"; 8 | 9 | public static final String CHANNELS = "/channels"; 10 | 11 | public static final String COMMON = "/"; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/constant/AttachmentConstant.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.constant; 2 | 3 | import lombok.Getter; 4 | 5 | public class AttachmentConstant { 6 | 7 | @Getter 8 | public enum Flags { 9 | SPOILER(1); 10 | 11 | private final long bit; 12 | 13 | Flags(long bit) { 14 | this.bit = bit; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/constant/AttributeConstant.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.constant; 2 | 3 | public class AttributeConstant { 4 | 5 | public static final String USER = "user"; 6 | 7 | public static final String MEMBER = "member"; 8 | 9 | public static final String CHANNEL = "channel"; 10 | 11 | public static final String ACCESS_TOKEN = "access_token"; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/constant/ChannelConstant.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.constant; 2 | 3 | import lombok.Getter; 4 | 5 | public class ChannelConstant { 6 | 7 | @Getter 8 | public enum Type { 9 | DM(1), 10 | GROUP(2), 11 | CHANNEL(3); 12 | 13 | private final int type; 14 | 15 | Type(int type) { 16 | this.type = type; 17 | } 18 | } 19 | 20 | @Getter 21 | public enum Flags { 22 | PUBLIC(1), 23 | BLOCKED(1 << 1); 24 | 25 | private final long bit; 26 | 27 | Flags(long bit) { 28 | this.bit = bit; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/constant/CloseCodeConstant.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.constant; 2 | 3 | import org.springframework.web.socket.CloseStatus; 4 | 5 | public class CloseCodeConstant { 6 | 7 | public static final CloseStatus UNAUTHORIZED = new CloseStatus(4001, "Unauthorized"); 8 | 9 | public static final CloseStatus HEARTBEAT_TIMEOUT = new CloseStatus(4002, "Heartbeat timeout"); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/constant/EmailConstant.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.constant; 2 | 3 | public class EmailConstant { 4 | 5 | public enum Type { 6 | RESET_PASSWORD("reset_password"), 7 | EMAIL_VERIFY("email_verify"), 8 | ACCOUNT_DELETE("account_delete"); 9 | 10 | private final String type; 11 | 12 | Type(String type) { 13 | this.type = type; 14 | } 15 | 16 | public String getValue() { 17 | return type; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/constant/ExceptionConstant.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.constant; 2 | 3 | public class ExceptionConstant { 4 | 5 | private static final int USER_ERROR = 100; 6 | 7 | private static final int CHANNEL_ERROR = 200; 8 | 9 | private static final int MEMBER_ERROR = 300; 10 | 11 | private static final int MESSAGE_ERROR = 400; 12 | 13 | private static final int OTP_ERROR = 500; 14 | 15 | private static final int CDN_ERROR = 700; 16 | 17 | private static final int API_ERROR = 800; 18 | 19 | private static final int UNKNOWN_ERROR = 900; 20 | 21 | public enum Messages { 22 | SERVER_EXCEPTION("Server exception ({}, {}, {}) occurred"), 23 | INTERNAL_ERROR("An internal server error has occurred"), 24 | REQUEST_BODY_EMPTY("Request body cannot be empty"), 25 | SERVER_EXCEPTION_STACKTRACE("Server exception stacktrace:"), 26 | UPLOAD_FAILED("Image upload failed"), 27 | INVALID_FILE_FORMAT("Invalid file format"), 28 | CHANNEL_ALREADY_EXIST("Channel with this name already exist"), 29 | CHANNEL_NOT_FOUND("Unknown channel"), 30 | OTP_EXPIRED("OTP has expired"), 31 | OTP_IS_INVALID("OTP is invalid"), 32 | NEED_TO_WAIT("You need to wait 1 minute to resend OTP again"), 33 | MEMBER_ALREADY_EXIST("You've already joined this channel"), 34 | MEMBER_NOT_FOUND("Can't find member in this channel"), 35 | MISSING_PERMISSIONS("You don't have enough permissions to perform this action"), 36 | MESSAGE_NOT_FOUND("Unable to find message(s) for this channel or matching these parameters"), 37 | MESSAGE_CANNOT_BE_EMPTY("Message cannot be empty"), 38 | ATTACHMENTS_CANNOT_BE_EMPTY("Attachments cannot be empty"), 39 | UNKNOWN_ATTACHMENTS("Unknown attachments ids"), 40 | USER_CREDENTIALS_DUPLICATE("User with this username/email already exist"), 41 | USER_CREDENTIALS_IS_INVALID("Invalid password or email"), 42 | USER_EMAIL_VERIFIED("You need to verify your email first"), 43 | USER_NOT_FOUND("Unknown user"), 44 | USER_UNAUTHORIZED("You need to authorize first"), 45 | ROUTE_NOT_FOUND("Route not found"), 46 | USER_CONTACT_ALREADY_EXIST("Contact already exist"), 47 | USER_CONTACT_NOT_FOUND("Contact not found"); 48 | 49 | private final String message; 50 | 51 | Messages(String message) { 52 | this.message = message; 53 | } 54 | 55 | public String getValue() { 56 | return message; 57 | } 58 | } 59 | 60 | public enum User { 61 | NOT_FOUND, 62 | EMAIL_NOT_VERIFIED, 63 | CREDENTIALS_DUPLICATE, 64 | CREDENTIALS_IS_INVALID, 65 | UNAUTHORIZED, 66 | CONTACT_ALREADY_EXIST, 67 | CONTACT_NOT_FOUND; 68 | 69 | public int getValue() { 70 | return USER_ERROR + this.ordinal(); 71 | } 72 | } 73 | 74 | public enum Channel { 75 | NOT_FOUND, 76 | ALREADY_EXIST; 77 | 78 | public int getValue() { 79 | return CHANNEL_ERROR + this.ordinal(); 80 | } 81 | } 82 | 83 | public enum Member { 84 | NOT_FOUND, 85 | ALREADY_EXIST, 86 | MISSING_PERMISSIONS; 87 | 88 | public int getValue() { 89 | return MEMBER_ERROR + this.ordinal(); 90 | } 91 | } 92 | 93 | public enum Message { 94 | NOT_FOUND, 95 | CANNOT_BE_EMPTY, 96 | ATTACHMENTS_CANNOT_BE_EMPTY, 97 | UNKNOWN_ATTACHMENTS; 98 | 99 | public int getValue() { 100 | return MESSAGE_ERROR + this.ordinal(); 101 | } 102 | } 103 | 104 | public enum OTP { 105 | IS_INVALID, 106 | EXPIRED, 107 | WAIT_TO_RESEND; 108 | 109 | public int getValue() { 110 | return OTP_ERROR + this.ordinal(); 111 | } 112 | } 113 | 114 | public enum CDN { 115 | UPLOAD_FAILED, 116 | INVALID_FILE_FORMAT; 117 | 118 | public int getValue() { 119 | return CDN_ERROR + this.ordinal(); 120 | } 121 | } 122 | 123 | public enum API { 124 | RATE_LIMIT_EXCEEDED, 125 | EMPTY_BODY, 126 | VALIDATION_ERROR, 127 | ROUTE_NOT_FOUND; 128 | 129 | public int getValue() { 130 | return API_ERROR + this.ordinal(); 131 | } 132 | } 133 | 134 | public enum Unknown { 135 | ERROR; 136 | 137 | public int getValue() { 138 | return UNKNOWN_ERROR + this.ordinal(); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/constant/GatewayConstant.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.constant; 2 | 3 | public class GatewayConstant { 4 | 5 | public static final int HEARTBEAT_INTERVAL = 30000; 6 | 7 | public static final int HEARTBEAT_TIMEOUT = 3; 8 | 9 | public enum Event { 10 | MESSAGE_CREATE("MESSAGE_CREATE"), 11 | MESSAGE_UPDATE("MESSAGE_UPDATE"), 12 | MESSAGE_DELETE("MESSAGE_DELETE"), 13 | CHANNEL_CREATE("CHANNEL_CREATE"), 14 | CHANNEL_UPDATE("CHANNEL_UPDATE"), 15 | CHANNEL_DELETE("CHANNEL_DELETE"), 16 | MEMBER_ADD("MEMBER_ADD"), 17 | MEMBER_REMOVE("MEMBER_REMOVE"), 18 | USER_STATUS_UPDATE("USER_STATUS_UPDATE"), 19 | USER_UPDATE("USER_UPDATE"), 20 | CONTACT_ADD("CONTACT_ADD"), 21 | CONTACT_DELETE("CONTACT_DELETE"), 22 | TYPING_START("TYPING_START"); 23 | 24 | private final String name; 25 | 26 | Event(String name) { 27 | this.name = name; 28 | } 29 | 30 | public String getValue() { 31 | return name; 32 | } 33 | } 34 | 35 | public enum Opcode { 36 | DISPATCH, // 0 37 | IDENTIFY, // 1 38 | HELLO, // 2 39 | HEARTBEAT, // 3 40 | HEARTBEAT_ACK, // 4 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/constant/MemberConstant.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.constant; 2 | 3 | import lombok.Getter; 4 | 5 | public class MemberConstant { 6 | 7 | @Getter 8 | public enum Permissions { 9 | ADMIN(1), 10 | BAN_MEMBERS(1 << 1), 11 | KICK_MEMBERS(1 << 2), 12 | MANAGE_MESSAGES(1 << 3), 13 | MANAGE_CHANNEL(1 << 4), 14 | ATTACH_FILES(1 << 5), 15 | SEND_MESSAGES(1 << 6); 16 | 17 | private final long bit; 18 | 19 | Permissions(long bit) { 20 | this.bit = bit; 21 | } 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/constant/OTPConstant.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.constant; 2 | 3 | public class OTPConstant { 4 | 5 | public enum Lifetime { 6 | BASE(3600000), 7 | RESEND(60000); 8 | 9 | private final long time; 10 | 11 | Lifetime(long time) { 12 | this.time = time; 13 | } 14 | 15 | public long getValue() { 16 | return time; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/constant/StorageConstant.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.constant; 2 | 3 | public class StorageConstant { 4 | 5 | public static final String AVATARS_BUCKET = "avatars"; 6 | 7 | public static final String ATTACHMENTS_BUCKET = "attachments"; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/constant/TokenConstant.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.constant; 2 | 3 | public class TokenConstant { 4 | 5 | public static final long LIFETIME = 2628000000L; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/constant/UserConstant.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.constant; 2 | 3 | import lombok.Getter; 4 | 5 | public class UserConstant { 6 | 7 | @Getter 8 | public enum Flags { 9 | AWAITING_CONFIRMATION(1), 10 | EMAIL_VERIFIED(1 << 1), 11 | DISABLED(1 << 2); 12 | 13 | private final long bit; 14 | 15 | Flags(long bit) { 16 | this.bit = bit; 17 | } 18 | } 19 | 20 | @Getter 21 | public enum Type { 22 | USER(1), 23 | BOT(2); 24 | 25 | private final int type; 26 | 27 | Type(int type) { 28 | this.type = type; 29 | } 30 | } 31 | 32 | @Getter 33 | public enum Status { 34 | ONLINE(1), 35 | OFFLINE(2); 36 | 37 | private final int status; 38 | 39 | Status(int status) { 40 | this.status = status; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/constant/ValidationConstant.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.constant; 2 | 3 | public class ValidationConstant { 4 | 5 | public static class Lengths { 6 | public static final int MIN = 4; 7 | 8 | public static final int PASSWORD = 128; 9 | 10 | public static final int DISPLAY_NAME = 32; 11 | 12 | public static final int USERNAME = 32; 13 | 14 | public static final int EMAIL = 64; 15 | 16 | public static final int CHANNEL_NAME = 16; 17 | 18 | public static final int MESSAGE_CONTENT = 5000; 19 | 20 | public static final int ATTACHMENTS_MAX = 10; 21 | } 22 | 23 | public static class Messages { 24 | public static final String PASSWORD_WRONG_LENGTH = "Password must be between {min} and {max} characters long"; 25 | 26 | public static final String DISPLAY_NAME_WRONG_LENGTH = "Display name must be between {min} and {max} characters long"; 27 | 28 | public static final String USERNAME_WRONG_LENGTH = "Username must be between {min} and {max} characters long"; 29 | 30 | public static final String USERNAME_INCORRECT = "Incorrect username format"; 31 | 32 | public static final String EMAIL_WRONG_LENGTH = "Email must be between {min} and {max} characters long"; 33 | 34 | public static final String EMAIL_INCORRECT = "Incorrect email format"; 35 | 36 | public static final String CHANNEL_NAME_WRONG_LENGTH = "Channel name must be between {min} and {max} characters long"; 37 | 38 | public static final String CHANNEL_NAME_INCORRECT = "Incorrect channel format"; 39 | 40 | public static final String CHANNEL_TYPE_INCORRECT = "Channel type are incorrect"; 41 | 42 | public static final String MESSAGE_WRONG_LENGTH = "Message length must be between {min} and {max} characters long"; 43 | 44 | public static final String ATTACHMENTS_WRONG_SIZE = "Message can contain only {max} attachments"; 45 | 46 | public static final String OTP_NAME_WRONG_LENGTH = "OTP must be {min} characters long"; 47 | 48 | public static final String MUST_NOT_BE_NULL = " must not be null"; 49 | 50 | public static final String USER_AVATAR_MUST_BE_POSITIVE = "User avatar id must be positive"; 51 | } 52 | 53 | public static class Regex { 54 | public static final String EMAIL_REGEX = "^[\\w+.-]+@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*\\.[A-Za-z]{2,}$"; 55 | 56 | public static final String NAME_REGEX = "^[A-Za-z0-9_-]+$"; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/controller/AuthenticationController.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.controller; 2 | 3 | import io.swagger.v3.oas.annotations.Operation; 4 | import io.swagger.v3.oas.annotations.security.SecurityRequirements; 5 | import io.swagger.v3.oas.annotations.tags.Tag; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.web.bind.annotation.*; 8 | import su.foxochat.constant.APIConstant; 9 | import su.foxochat.constant.AttributeConstant; 10 | import su.foxochat.dto.api.request.*; 11 | import su.foxochat.dto.api.response.OkDTO; 12 | import su.foxochat.dto.api.response.TokenDTO; 13 | import su.foxochat.exception.otp.NeedToWaitBeforeResendException; 14 | import su.foxochat.exception.otp.OTPExpiredException; 15 | import su.foxochat.exception.otp.OTPsInvalidException; 16 | import su.foxochat.exception.user.UserCredentialsDuplicateException; 17 | import su.foxochat.exception.user.UserCredentialsIsInvalidException; 18 | import su.foxochat.model.User; 19 | import su.foxochat.service.AuthenticationService; 20 | 21 | @Slf4j 22 | @RestController 23 | @Tag(name = "Authentication") 24 | @RequestMapping(value = APIConstant.AUTH, produces = "application/json") 25 | public class AuthenticationController { 26 | 27 | private final AuthenticationService authenticationService; 28 | 29 | public AuthenticationController(AuthenticationService authenticationService) { 30 | this.authenticationService = authenticationService; 31 | } 32 | 33 | @Operation(summary = "Register") 34 | @SecurityRequirements 35 | @PostMapping("/register") 36 | public TokenDTO register(@RequestBody UserRegisterDTO body) throws UserCredentialsDuplicateException { 37 | String username = body.getUsername(); 38 | String email = body.getEmail(); 39 | String password = body.getPassword(); 40 | String accessToken = authenticationService.register(username, email, password); 41 | 42 | return new TokenDTO(accessToken); 43 | } 44 | 45 | @Operation(summary = "Login") 46 | @SecurityRequirements 47 | @PostMapping("/login") 48 | public TokenDTO login(@RequestBody UserLoginDTO body) throws UserCredentialsIsInvalidException { 49 | String email = body.getEmail(); 50 | String password = body.getPassword(); 51 | 52 | String accessToken = authenticationService.login(email, password); 53 | 54 | return new TokenDTO(accessToken); 55 | } 56 | 57 | @Operation(summary = "Verify email") 58 | @PostMapping("/email/verify") 59 | public OkDTO emailVerify(@RequestAttribute(value = AttributeConstant.USER) User user, @RequestBody OTPDTO body) throws OTPsInvalidException, OTPExpiredException { 60 | authenticationService.verifyEmail(user, body.getOTP()); 61 | 62 | return new OkDTO(true); 63 | } 64 | 65 | @Operation(summary = "Resend email") 66 | @PostMapping("/email/resend") 67 | public OkDTO resendEmail(@RequestAttribute(value = AttributeConstant.USER) User user, @RequestAttribute(value = AttributeConstant.ACCESS_TOKEN) String accessToken) throws OTPsInvalidException, NeedToWaitBeforeResendException { 68 | authenticationService.resendEmail(user, accessToken); 69 | 70 | return new OkDTO(true); 71 | } 72 | 73 | @Operation(summary = "Reset password") 74 | @SecurityRequirements 75 | @PostMapping("/reset-password") 76 | public OkDTO resetPassword(@RequestBody UserResetPasswordDTO body) throws UserCredentialsIsInvalidException { 77 | authenticationService.resetPassword(body); 78 | 79 | return new OkDTO(true); 80 | } 81 | 82 | @Operation(summary = "Confirm reset password") 83 | @SecurityRequirements 84 | @PostMapping("/reset-password/confirm") 85 | public OkDTO confirmResetPassword(@RequestBody UserResetPasswordConfirmDTO body) throws OTPExpiredException, OTPsInvalidException, UserCredentialsIsInvalidException { 86 | authenticationService.confirmResetPassword(body); 87 | 88 | return new OkDTO(true); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/controller/CommonController.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.controller; 2 | 3 | import io.swagger.v3.oas.annotations.Hidden; 4 | import io.swagger.v3.oas.annotations.Operation; 5 | import io.swagger.v3.oas.annotations.tags.Tag; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import su.foxochat.config.APIConfig; 11 | import su.foxochat.constant.APIConstant; 12 | import su.foxochat.dto.api.response.InfoDTO; 13 | 14 | @Slf4j 15 | @RestController 16 | @Tag(name = "Common") 17 | @RequestMapping(value = APIConstant.COMMON, produces = "application/json") 18 | public class CommonController { 19 | 20 | private final APIConfig apiConfig; 21 | 22 | public CommonController(APIConfig apiConfig) { 23 | this.apiConfig = apiConfig; 24 | } 25 | 26 | @Operation(summary = "Get info") 27 | @GetMapping("/info") 28 | public InfoDTO get() { 29 | String gatewayURL = apiConfig.isDevelopment() ? apiConfig.getDevGatewayURL() : apiConfig.getGatewayURL(); 30 | String appURL = apiConfig.isDevelopment() ? apiConfig.getDevAppURL() : apiConfig.getAppURL(); 31 | 32 | return new InfoDTO(apiConfig.getVersion(), apiConfig.getCdnURL(), gatewayURL, appURL); 33 | } 34 | 35 | @SuppressWarnings("SameReturnValue") 36 | @Hidden 37 | @GetMapping("/actuator/health") 38 | public String health() { 39 | return "{\"status\":\"UP\"}"; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.controller; 2 | 3 | import io.swagger.v3.oas.annotations.Operation; 4 | import io.swagger.v3.oas.annotations.tags.Tag; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.web.bind.annotation.*; 7 | import su.foxochat.constant.APIConstant; 8 | import su.foxochat.constant.AttributeConstant; 9 | import su.foxochat.dto.api.request.AttachmentAddDTO; 10 | import su.foxochat.dto.api.request.OTPDTO; 11 | import su.foxochat.dto.api.request.UserDeleteDTO; 12 | import su.foxochat.dto.api.request.UserEditDTO; 13 | import su.foxochat.dto.api.response.ChannelDTO; 14 | import su.foxochat.dto.api.response.OkDTO; 15 | import su.foxochat.dto.api.response.UploadAttachmentDTO; 16 | import su.foxochat.dto.api.response.UserDTO; 17 | import su.foxochat.dto.internal.AttachmentPresignedDTO; 18 | import su.foxochat.exception.message.AttachmentsCannotBeEmpty; 19 | import su.foxochat.exception.message.UnknownAttachmentsException; 20 | import su.foxochat.exception.otp.OTPExpiredException; 21 | import su.foxochat.exception.otp.OTPsInvalidException; 22 | import su.foxochat.exception.user.UserContactAlreadyExistException; 23 | import su.foxochat.exception.user.UserContactNotFoundException; 24 | import su.foxochat.exception.user.UserCredentialsIsInvalidException; 25 | import su.foxochat.exception.user.UserNotFoundException; 26 | import su.foxochat.model.Channel; 27 | import su.foxochat.model.Message; 28 | import su.foxochat.model.User; 29 | import su.foxochat.service.AttachmentService; 30 | import su.foxochat.service.MemberService; 31 | import su.foxochat.service.MessageService; 32 | import su.foxochat.service.UserService; 33 | 34 | import java.util.List; 35 | import java.util.stream.Collectors; 36 | 37 | @Slf4j 38 | @RestController 39 | @Tag(name = "Users") 40 | @RequestMapping(value = APIConstant.USERS, produces = "application/json") 41 | public class UserController { 42 | 43 | private final UserService userService; 44 | 45 | private final MemberService memberService; 46 | 47 | private final MessageService messageService; 48 | 49 | private final AttachmentService attachmentService; 50 | 51 | public UserController(UserService userService, MemberService memberService, MessageService messageService, AttachmentService attachmentService) { 52 | this.userService = userService; 53 | this.memberService = memberService; 54 | this.messageService = messageService; 55 | this.attachmentService = attachmentService; 56 | } 57 | 58 | @Operation(summary = "Get me") 59 | @GetMapping("/@me") 60 | public UserDTO getMe(@RequestAttribute(value = AttributeConstant.USER) User user) { 61 | List channels = memberService.getChannelsByUserId(user.getId()) 62 | .stream() 63 | .map(Channel::getId) 64 | .collect(Collectors.toList()); 65 | 66 | List contacts = user.getContacts().stream().map(userContact -> userContact.getContact().getId()).toList(); 67 | 68 | return new UserDTO(user, channels, contacts, true, true, true); 69 | } 70 | 71 | @Operation(summary = "Get user by id") 72 | @GetMapping("/{id}") 73 | public UserDTO getById(@PathVariable long id) throws UserNotFoundException { 74 | return new UserDTO(userService.getById(id).orElseThrow(UserNotFoundException::new), 75 | null, 76 | null, false, 77 | false, false); 78 | } 79 | 80 | @Operation(summary = "Get user by username") 81 | @GetMapping("/@{username}") 82 | public UserDTO getByUsername(@PathVariable String username) throws UserNotFoundException { 83 | return new UserDTO(userService.getByUsername(username).orElseThrow(UserNotFoundException::new), null, null, false, false, false); 84 | } 85 | 86 | @Operation(summary = "Get user channels") 87 | @GetMapping("/@me/channels") 88 | public List getChannels(@RequestAttribute(value = AttributeConstant.USER) User authenticatedUser) { 89 | return memberService.getChannelsByUserId(authenticatedUser.getId()) 90 | .stream() 91 | .map(channel -> { 92 | Message lastMessage = messageService.getLastByChannel(channel); 93 | return new ChannelDTO(channel, lastMessage); 94 | }) 95 | .collect(Collectors.toList()); 96 | } 97 | 98 | @Operation(summary = "Edit user") 99 | @PatchMapping("/@me") 100 | public UserDTO edit(@RequestAttribute(value = AttributeConstant.USER) User authenticatedUser, @RequestBody UserEditDTO body) throws Exception { 101 | authenticatedUser = userService.update(authenticatedUser, body); 102 | 103 | return new UserDTO(authenticatedUser, null, null, true, true, false); 104 | } 105 | 106 | @Operation(summary = "Upload avatar") 107 | @PutMapping("/@me/avatar") 108 | public UploadAttachmentDTO uploadAvatar(@RequestAttribute(value = AttributeConstant.USER) User authenticatedUser, @RequestBody AttachmentAddDTO attachment) throws UnknownAttachmentsException, AttachmentsCannotBeEmpty { 109 | AttachmentPresignedDTO data = attachmentService.upload(authenticatedUser, attachment); 110 | 111 | return new UploadAttachmentDTO(data.getUrl(), data.getAttachment().getId()); 112 | } 113 | 114 | @Operation(summary = "Delete") 115 | @DeleteMapping("/@me") 116 | public OkDTO delete(@RequestAttribute(value = AttributeConstant.USER) User user, @RequestBody UserDeleteDTO body) throws UserCredentialsIsInvalidException { 117 | String password = body.getPassword(); 118 | 119 | userService.requestDelete(user, password); 120 | 121 | return new OkDTO(true); 122 | } 123 | 124 | @Operation(summary = "Confirm delete") 125 | @PostMapping("/@me/delete-confirm") 126 | public OkDTO deleteConfirm(@RequestAttribute(value = AttributeConstant.USER) User user, @RequestBody OTPDTO body) throws OTPExpiredException, OTPsInvalidException { 127 | userService.confirmDelete(user, body.getOTP()); 128 | 129 | return new OkDTO(true); 130 | } 131 | 132 | @Operation(summary = "Get contacts") 133 | @GetMapping("/@me/contacts") 134 | public List getContacts(@RequestAttribute(value = AttributeConstant.USER) User authenticatedUser) throws UserNotFoundException { 135 | User user = userService.getById(authenticatedUser.getId()).orElseThrow(UserNotFoundException::new); 136 | return user.getContacts() 137 | .stream() 138 | .map(contact -> new UserDTO(contact.getContact(), null, null, false, false, false)) 139 | .collect(Collectors.toList()); 140 | } 141 | 142 | @Operation(summary = "Add contact") 143 | @PostMapping("/{id}") 144 | public UserDTO addContact(@RequestAttribute(value = AttributeConstant.USER) User user, @PathVariable long id) throws UserContactAlreadyExistException { 145 | return new UserDTO(userService.addContact(user, id), null, null, false, false, false); 146 | } 147 | 148 | @Operation(summary = "Delete contact") 149 | @DeleteMapping("/{id}") 150 | public OkDTO deleteContact(@RequestAttribute(value = AttributeConstant.USER) User user, @PathVariable long id) throws UserContactNotFoundException { 151 | userService.deleteContact(user, id); 152 | 153 | return new OkDTO(true); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/request/AttachmentAddDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.request; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | 8 | @Setter 9 | @Getter 10 | @AllArgsConstructor 11 | @Schema(name = "AttachmentsAdd") 12 | public class AttachmentAddDTO { 13 | 14 | private String filename; 15 | 16 | private String contentType; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/request/ChannelCreateDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.request; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.*; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import su.foxochat.constant.ValidationConstant; 8 | 9 | @Setter 10 | @Getter 11 | @Schema(name = "ChannelCreate") 12 | public class ChannelCreateDTO { 13 | 14 | @NotNull(message = "Display name" + ValidationConstant.Messages.MUST_NOT_BE_NULL) 15 | @Size(min = 1, max = ValidationConstant.Lengths.CHANNEL_NAME, message = ValidationConstant.Messages.CHANNEL_NAME_WRONG_LENGTH) 16 | private String displayName; 17 | 18 | @NotNull(message = "Name" + ValidationConstant.Messages.MUST_NOT_BE_NULL) 19 | @Pattern(regexp = ValidationConstant.Regex.NAME_REGEX, message = ValidationConstant.Messages.CHANNEL_NAME_INCORRECT) 20 | @Size(min = 1, max = ValidationConstant.Lengths.CHANNEL_NAME, message = ValidationConstant.Messages.CHANNEL_NAME_WRONG_LENGTH) 21 | private String name; 22 | 23 | @NotNull(message = "Type" + ValidationConstant.Messages.MUST_NOT_BE_NULL) 24 | @Min(value = 1, message = ValidationConstant.Messages.CHANNEL_TYPE_INCORRECT) 25 | @Max(value = 3, message = ValidationConstant.Messages.CHANNEL_TYPE_INCORRECT) 26 | private int type; 27 | 28 | @NotNull(message = "Public" + ValidationConstant.Messages.MUST_NOT_BE_NULL) 29 | private boolean isPublic; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/request/ChannelEditDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.request; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.Pattern; 5 | import jakarta.validation.constraints.Size; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import su.foxochat.constant.ValidationConstant; 9 | 10 | @Getter 11 | @Setter 12 | @Schema(name = "ChannelEdit") 13 | public class ChannelEditDTO { 14 | 15 | @Size(min = 1, max = ValidationConstant.Lengths.CHANNEL_NAME, message = ValidationConstant.Messages.CHANNEL_NAME_WRONG_LENGTH) 16 | private String displayName; 17 | 18 | @Pattern(regexp = ValidationConstant.Regex.NAME_REGEX, message = ValidationConstant.Messages.CHANNEL_NAME_INCORRECT) 19 | @Size(min = 1, max = ValidationConstant.Lengths.CHANNEL_NAME, message = ValidationConstant.Messages.CHANNEL_NAME_WRONG_LENGTH) 20 | private String name; 21 | 22 | private long icon; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/request/MessageCreateDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.request; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.Size; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import su.foxochat.constant.ValidationConstant; 8 | 9 | import java.util.List; 10 | 11 | @Setter 12 | @Getter 13 | @Schema(name = "MessageCreate") 14 | public class MessageCreateDTO { 15 | 16 | @Size(max = ValidationConstant.Lengths.MESSAGE_CONTENT, message = ValidationConstant.Messages.MESSAGE_WRONG_LENGTH) 17 | private String content; 18 | 19 | @Size(max = ValidationConstant.Lengths.ATTACHMENTS_MAX, message = ValidationConstant.Messages.ATTACHMENTS_WRONG_SIZE) 20 | private List attachments; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/request/OTPDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.request; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotNull; 5 | import jakarta.validation.constraints.Size; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import su.foxochat.constant.ValidationConstant; 9 | 10 | @Setter 11 | @Getter 12 | @Schema(name = "OTP") 13 | public class OTPDTO { 14 | 15 | @NotNull(message = "OTP" + ValidationConstant.Messages.MUST_NOT_BE_NULL) 16 | @Size(min = 6, max = 6, message = ValidationConstant.Messages.OTP_NAME_WRONG_LENGTH) 17 | private String OTP; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/request/UserDeleteDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.request; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotNull; 5 | import jakarta.validation.constraints.Size; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import su.foxochat.constant.ValidationConstant; 9 | 10 | @Setter 11 | @Getter 12 | @Schema(name = "UserDelete") 13 | public class UserDeleteDTO { 14 | 15 | @NotNull(message = "Password" + ValidationConstant.Messages.MUST_NOT_BE_NULL) 16 | @Size(min = ValidationConstant.Lengths.MIN, max = ValidationConstant.Lengths.PASSWORD, message = ValidationConstant.Messages.PASSWORD_WRONG_LENGTH) 17 | private String password; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/request/UserEditDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.request; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.Pattern; 5 | import jakarta.validation.constraints.Positive; 6 | import jakarta.validation.constraints.Size; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | import su.foxochat.constant.ValidationConstant; 10 | 11 | @Getter 12 | @Setter 13 | @Schema(name = "UserEdit") 14 | public class UserEditDTO { 15 | 16 | @Size(min = ValidationConstant.Lengths.MIN, max = ValidationConstant.Lengths.DISPLAY_NAME, message = ValidationConstant.Messages.DISPLAY_NAME_WRONG_LENGTH) 17 | private String displayName; 18 | 19 | @Size(min = ValidationConstant.Lengths.MIN, max = ValidationConstant.Lengths.USERNAME, message = ValidationConstant.Messages.USERNAME_WRONG_LENGTH) 20 | @Pattern(regexp = ValidationConstant.Regex.NAME_REGEX, message = ValidationConstant.Messages.USERNAME_INCORRECT) 21 | private String username; 22 | 23 | @Size(min = ValidationConstant.Lengths.MIN, max = ValidationConstant.Lengths.EMAIL, message = ValidationConstant.Messages.EMAIL_WRONG_LENGTH) 24 | @Pattern(regexp = ValidationConstant.Regex.EMAIL_REGEX, message = ValidationConstant.Messages.EMAIL_INCORRECT) 25 | private String email; 26 | 27 | @Size(min = ValidationConstant.Lengths.MIN, max = ValidationConstant.Lengths.PASSWORD, message = ValidationConstant.Messages.PASSWORD_WRONG_LENGTH) 28 | private String password; 29 | 30 | @Positive(message = ValidationConstant.Messages.USER_AVATAR_MUST_BE_POSITIVE) 31 | private Long avatar; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/request/UserLoginDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.request; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotNull; 5 | import jakarta.validation.constraints.Pattern; 6 | import jakarta.validation.constraints.Size; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | import su.foxochat.constant.ValidationConstant; 10 | 11 | @Setter 12 | @Getter 13 | @Schema(name = "UserLogin") 14 | public class UserLoginDTO { 15 | 16 | @NotNull(message = "Email" + ValidationConstant.Messages.MUST_NOT_BE_NULL) 17 | @Size(min = ValidationConstant.Lengths.MIN, max = ValidationConstant.Lengths.EMAIL, message = ValidationConstant.Messages.EMAIL_WRONG_LENGTH) 18 | @Pattern(regexp = ValidationConstant.Regex.EMAIL_REGEX, message = ValidationConstant.Messages.EMAIL_INCORRECT) 19 | private String email; 20 | 21 | @NotNull(message = "Password" + ValidationConstant.Messages.MUST_NOT_BE_NULL) 22 | @Size(min = ValidationConstant.Lengths.MIN, max = ValidationConstant.Lengths.PASSWORD, message = ValidationConstant.Messages.PASSWORD_WRONG_LENGTH) 23 | private String password; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/request/UserRegisterDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.request; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotNull; 5 | import jakarta.validation.constraints.Pattern; 6 | import jakarta.validation.constraints.Size; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | import su.foxochat.constant.ValidationConstant; 10 | 11 | @Setter 12 | @Getter 13 | @Schema(name = "UserRegister") 14 | public class UserRegisterDTO { 15 | 16 | @NotNull(message = "Username" + ValidationConstant.Messages.MUST_NOT_BE_NULL) 17 | @Size(min = ValidationConstant.Lengths.MIN, max = ValidationConstant.Lengths.USERNAME, message = ValidationConstant.Messages.USERNAME_WRONG_LENGTH) 18 | @Pattern(regexp = ValidationConstant.Regex.NAME_REGEX, message = ValidationConstant.Messages.USERNAME_INCORRECT) 19 | private String username; 20 | 21 | @NotNull(message = "Email" + ValidationConstant.Messages.MUST_NOT_BE_NULL) 22 | @Size(min = ValidationConstant.Lengths.MIN, max = ValidationConstant.Lengths.EMAIL, message = ValidationConstant.Messages.EMAIL_WRONG_LENGTH) 23 | @Pattern(regexp = ValidationConstant.Regex.EMAIL_REGEX, message = ValidationConstant.Messages.EMAIL_INCORRECT) 24 | private String email; 25 | 26 | @NotNull(message = "Password" + ValidationConstant.Messages.MUST_NOT_BE_NULL) 27 | @Size(min = ValidationConstant.Lengths.MIN, max = ValidationConstant.Lengths.PASSWORD, message = ValidationConstant.Messages.PASSWORD_WRONG_LENGTH) 28 | private String password; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/request/UserResetPasswordConfirmDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.request; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotNull; 5 | import jakarta.validation.constraints.Pattern; 6 | import jakarta.validation.constraints.Size; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | import su.foxochat.constant.ValidationConstant; 10 | 11 | @Getter 12 | @Setter 13 | @Schema(name = "UserResetPasswordConfirm") 14 | public class UserResetPasswordConfirmDTO { 15 | 16 | @NotNull(message = "Email" + ValidationConstant.Messages.MUST_NOT_BE_NULL) 17 | @Size(min = ValidationConstant.Lengths.MIN, max = ValidationConstant.Lengths.EMAIL, message = ValidationConstant.Messages.EMAIL_WRONG_LENGTH) 18 | @Pattern(regexp = ValidationConstant.Regex.EMAIL_REGEX, message = ValidationConstant.Messages.EMAIL_INCORRECT) 19 | private String email; 20 | 21 | @NotNull(message = "OTP" + ValidationConstant.Messages.MUST_NOT_BE_NULL) 22 | @Size(min = 6, max = 6, message = ValidationConstant.Messages.OTP_NAME_WRONG_LENGTH) 23 | private String OTP; 24 | 25 | @NotNull(message = "Password" + ValidationConstant.Messages.MUST_NOT_BE_NULL) 26 | @Size(min = ValidationConstant.Lengths.MIN, max = ValidationConstant.Lengths.PASSWORD, message = ValidationConstant.Messages.PASSWORD_WRONG_LENGTH) 27 | private String newPassword; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/request/UserResetPasswordDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.request; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotNull; 5 | import jakarta.validation.constraints.Pattern; 6 | import jakarta.validation.constraints.Size; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | import su.foxochat.constant.ValidationConstant; 10 | 11 | @Setter 12 | @Getter 13 | @Schema(name = "UserResetPassword") 14 | public class UserResetPasswordDTO { 15 | 16 | @NotNull(message = "Email" + ValidationConstant.Messages.MUST_NOT_BE_NULL) 17 | @Size(min = ValidationConstant.Lengths.MIN, max = ValidationConstant.Lengths.EMAIL, message = ValidationConstant.Messages.EMAIL_WRONG_LENGTH) 18 | @Pattern(regexp = ValidationConstant.Regex.EMAIL_REGEX, message = ValidationConstant.Messages.EMAIL_INCORRECT) 19 | private String email; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/response/AttachmentDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.response; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import su.foxochat.model.Attachment; 7 | 8 | @Getter 9 | @Setter 10 | @Schema(name = "Attachment") 11 | public class AttachmentDTO { 12 | 13 | public long id; 14 | 15 | public String uuid; 16 | 17 | public String filename; 18 | 19 | public String contentType; 20 | 21 | public long flags; 22 | 23 | public AttachmentDTO(Attachment attachment) { 24 | this.id = attachment.getId(); 25 | this.uuid = attachment.getUuid(); 26 | this.filename = attachment.getFilename(); 27 | this.contentType = attachment.getContentType(); 28 | this.flags = attachment.getFlags(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/response/ChannelDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import io.swagger.v3.oas.annotations.media.Schema; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import su.foxochat.model.Channel; 8 | import su.foxochat.model.Message; 9 | 10 | @Getter 11 | @Setter 12 | @Schema(name = "Channel") 13 | public class ChannelDTO { 14 | 15 | private long id; 16 | 17 | private String displayName; 18 | 19 | private String name; 20 | 21 | private AttachmentDTO icon; 22 | 23 | private int type; 24 | 25 | private long flags; 26 | 27 | private int memberCount; 28 | 29 | private UserDTO owner; 30 | 31 | private long createdAt; 32 | 33 | @JsonInclude(JsonInclude.Include.NON_NULL) 34 | private MessageDTO lastMessage; 35 | 36 | public ChannelDTO(Channel channel, Message lastMessage) { 37 | this.id = channel.getId(); 38 | this.displayName = channel.getDisplayName(); 39 | this.name = channel.getName(); 40 | if (channel.getIcon() != null) { 41 | this.icon = new AttachmentDTO(channel.getIcon()); 42 | } 43 | this.type = channel.getType(); 44 | this.flags = channel.getFlags(); 45 | if (channel.getMembers() != null) { 46 | this.memberCount = channel.getMembers().size(); 47 | } 48 | if (lastMessage != null) { 49 | this.lastMessage = new MessageDTO(lastMessage, false); 50 | } 51 | this.owner = new UserDTO(channel.getOwner(), null, null, false, false, false); 52 | this.createdAt = channel.getCreatedAt(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/response/ExceptionDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.response; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class ExceptionDTO { 9 | 10 | private boolean ok; 11 | 12 | private int code; 13 | 14 | private String message; 15 | 16 | public ExceptionDTO(boolean ok, int code, String message) { 17 | this.ok = ok; 18 | this.code = code; 19 | this.message = message; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/response/InfoDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.response; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | @Schema(name = "Info") 10 | public class InfoDTO { 11 | 12 | private String version; 13 | 14 | private String cdnURL; 15 | 16 | private String gatewayURL; 17 | 18 | private String appURL; 19 | 20 | public InfoDTO(String version, String cdnURL, String gatewayURL, String appURL) { 21 | this.version = version; 22 | this.cdnURL = cdnURL; 23 | this.gatewayURL = gatewayURL; 24 | this.appURL = appURL; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/response/MemberDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import io.swagger.v3.oas.annotations.media.Schema; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import su.foxochat.model.Member; 8 | 9 | @Getter 10 | @Setter 11 | @Schema(name = "Member") 12 | public class MemberDTO { 13 | 14 | private long id; 15 | 16 | private UserDTO user; 17 | 18 | @JsonInclude(JsonInclude.Include.NON_NULL) 19 | private ChannelDTO channel; 20 | 21 | private long permissions; 22 | 23 | private long joinedAt; 24 | 25 | public MemberDTO(Member member, boolean includeChannel) { 26 | this.id = member.getId(); 27 | this.user = new UserDTO(member.getUser(), null, null, false, false, false); 28 | if (includeChannel) this.channel = new ChannelDTO(member.getChannel(), null); 29 | this.permissions = member.getPermissions(); 30 | this.joinedAt = member.getJoinedAt(); 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/response/MessageDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.response; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import su.foxochat.model.Message; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | @Getter 13 | @Setter 14 | @Schema(name = "Message") 15 | public class MessageDTO { 16 | 17 | private long id; 18 | 19 | private String content; 20 | 21 | private MemberDTO author; 22 | 23 | private ChannelDTO channel; 24 | 25 | private List attachments; 26 | 27 | private long createdAt; 28 | 29 | public MessageDTO(Message message, boolean includeChannel) { 30 | this.id = message.getId(); 31 | this.content = message.getContent(); 32 | this.author = new MemberDTO(message.getAuthor(), false); 33 | if (includeChannel) this.channel = new ChannelDTO(message.getChannel(), null); 34 | if (message.getAttachments() != null) this.attachments = message.getAttachments().stream() 35 | .map(messageAttachment -> new AttachmentDTO(messageAttachment.getAttachment())) 36 | .collect(Collectors.toList()); 37 | else this.attachments = new ArrayList<>(); 38 | this.createdAt = message.getTimestamp(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/response/MessagesDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.response; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import su.foxochat.model.Message; 7 | 8 | import java.util.List; 9 | 10 | @Getter 11 | @Setter 12 | @Schema(name = "Messages") 13 | public class MessagesDTO { 14 | 15 | private List messages; 16 | 17 | public MessagesDTO(List messages) { 18 | for (Message message : messages) { 19 | this.messages.add(new MessageDTO(message, true)); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/response/OkDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.response; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | @Schema(name = "Ok") 10 | public class OkDTO { 11 | 12 | private boolean ok; 13 | 14 | public OkDTO(boolean ok) { 15 | this.ok = ok; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/response/TokenDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.response; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | @Schema(name = "Token") 10 | public class TokenDTO { 11 | 12 | private String accessToken; 13 | 14 | public TokenDTO(String accessToken) { 15 | this.accessToken = accessToken; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/response/UploadAttachmentDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.response; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | @Schema(name = "Attachments") 10 | public class UploadAttachmentDTO { 11 | 12 | public String url; 13 | 14 | public long id; 15 | 16 | public UploadAttachmentDTO(String url, long id) { 17 | this.url = url; 18 | this.id = id; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/api/response/UserDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.api.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import io.swagger.v3.oas.annotations.media.Schema; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import su.foxochat.model.User; 8 | 9 | import java.util.List; 10 | 11 | @Getter 12 | @Setter 13 | @Schema(name = "User") 14 | public class UserDTO { 15 | 16 | private long id; 17 | 18 | private AttachmentDTO avatar; 19 | 20 | private String displayName; 21 | 22 | private String username; 23 | 24 | @JsonInclude(JsonInclude.Include.NON_NULL) 25 | private String email; 26 | 27 | @JsonInclude(JsonInclude.Include.NON_NULL) 28 | private List channels; 29 | 30 | private int status; 31 | 32 | private long statusUpdatedAt; 33 | 34 | @JsonInclude(JsonInclude.Include.NON_NULL) 35 | private List contacts; 36 | 37 | private long flags; 38 | 39 | private int type; 40 | 41 | private long createdAt; 42 | 43 | @SuppressWarnings("unused") 44 | public UserDTO() {} 45 | 46 | public UserDTO(User user, List channels, List contacts, boolean includeEmail, boolean includeChannels, boolean includeContacts) { 47 | this.id = user.getId(); 48 | if (user.getAvatar() != null) { 49 | this.avatar = new AttachmentDTO(user.getAvatar()); 50 | } 51 | this.displayName = user.getDisplayName(); 52 | this.username = user.getUsername(); 53 | if (includeEmail) { 54 | this.email = user.getEmail(); 55 | } 56 | if (includeChannels) { 57 | this.channels = channels; 58 | } 59 | if (includeContacts) { 60 | this.contacts = contacts; 61 | } 62 | this.status = user.getStatus(); 63 | this.statusUpdatedAt = user.getStatusUpdatedAt(); 64 | this.flags = user.getFlags(); 65 | this.type = user.getType(); 66 | this.createdAt = user.getCreatedAt(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/gateway/EventDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.gateway; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class EventDTO { 9 | 10 | private int op; 11 | 12 | private Object d; 13 | 14 | private int s; 15 | 16 | private String t; 17 | 18 | public EventDTO() { 19 | } 20 | 21 | public EventDTO(int opcode, Object data, int sequence, String type) { 22 | this.op = opcode; 23 | this.d = data; 24 | this.s = sequence; 25 | this.t = type; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/gateway/StatusDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.gateway; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class StatusDTO { 9 | 10 | private long userId; 11 | 12 | private int status; 13 | 14 | public StatusDTO() { 15 | 16 | } 17 | 18 | public StatusDTO(long userId, int status) { 19 | this.userId = userId; 20 | this.status = status; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/gateway/response/ExceptionDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.gateway.response; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class ExceptionDTO { 9 | 10 | private int op; 11 | 12 | private String m; 13 | 14 | public ExceptionDTO(int opcode, String message) { 15 | this.op = opcode; 16 | this.m = message; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/gateway/response/HeartbeatACKDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.gateway.response; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import su.foxochat.constant.GatewayConstant; 6 | 7 | @Getter 8 | @Setter 9 | public class HeartbeatACKDTO { 10 | 11 | private int op; 12 | 13 | public HeartbeatACKDTO() { 14 | this.op = GatewayConstant.Opcode.HEARTBEAT_ACK.ordinal(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/gateway/response/HelloDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.gateway.response; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import su.foxochat.constant.GatewayConstant; 6 | 7 | import java.util.Map; 8 | 9 | @Getter 10 | @Setter 11 | public class HelloDTO { 12 | 13 | private int op; 14 | 15 | private Map d; 16 | 17 | public HelloDTO() { 18 | this.op = GatewayConstant.Opcode.HELLO.ordinal(); 19 | this.d = Map.of("heartbeat_interval", GatewayConstant.HEARTBEAT_INTERVAL); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/gateway/response/TypingStartDTO.java: -------------------------------------------------------------------------------- 1 | 2 | package su.foxochat.dto.gateway.response; 3 | 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import su.foxochat.constant.GatewayConstant; 7 | 8 | import java.util.Map; 9 | 10 | @Getter 11 | @Setter 12 | public class TypingStartDTO { 13 | 14 | private int op; 15 | 16 | private Map d; 17 | 18 | public TypingStartDTO(long channelId, long userId, long timestamp) { 19 | this.op = GatewayConstant.Opcode.HELLO.ordinal(); 20 | this.d = Map.of("channel_id", channelId, "user_id", userId, "timestamp", timestamp); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/dto/internal/AttachmentPresignedDTO.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.dto.internal; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import su.foxochat.model.Attachment; 6 | 7 | @Getter 8 | @Setter 9 | public class AttachmentPresignedDTO { 10 | 11 | private String url; 12 | 13 | private String uuid; 14 | 15 | private Attachment attachment; 16 | 17 | public AttachmentPresignedDTO(String url, String uuid, Attachment attachment) { 18 | this.url = url; 19 | this.uuid = uuid; 20 | this.attachment = attachment; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/BaseException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Setter 8 | public abstract class BaseException extends Exception { 9 | 10 | @Getter 11 | public int errorCode; 12 | 13 | @Getter 14 | public HttpStatus status; 15 | 16 | public String message; 17 | 18 | public BaseException(String message, HttpStatus status, int errorCode) { 19 | this.message = message; 20 | this.status = status; 21 | this.errorCode = errorCode; 22 | } 23 | 24 | @Override 25 | public String getMessage() { 26 | return message; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/cdn/InvalidFileFormatException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.cdn; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.NOT_ACCEPTABLE) 9 | public class InvalidFileFormatException extends BaseException { 10 | 11 | public InvalidFileFormatException() { 12 | super(ExceptionConstant.Messages.INVALID_FILE_FORMAT.getValue(), InvalidFileFormatException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.CDN.INVALID_FILE_FORMAT.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/cdn/UploadFailedException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.cdn; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 9 | public class UploadFailedException extends BaseException { 10 | 11 | public UploadFailedException() { 12 | super(ExceptionConstant.Messages.UPLOAD_FAILED.getValue(), UploadFailedException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.CDN.UPLOAD_FAILED.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/channel/ChannelAlreadyExistException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.channel; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.BAD_REQUEST) 9 | public class ChannelAlreadyExistException extends BaseException { 10 | 11 | public ChannelAlreadyExistException() { 12 | super(ExceptionConstant.Messages.CHANNEL_ALREADY_EXIST.getValue(), ChannelAlreadyExistException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.Channel.ALREADY_EXIST.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/channel/ChannelNotFoundException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.channel; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.NOT_FOUND) 9 | public class ChannelNotFoundException extends BaseException { 10 | 11 | public ChannelNotFoundException() { 12 | super(ExceptionConstant.Messages.CHANNEL_NOT_FOUND.getValue(), ChannelNotFoundException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.Channel.NOT_FOUND.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/member/MemberAlreadyInChannelException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.member; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.NOT_FOUND) 9 | public class MemberAlreadyInChannelException extends BaseException { 10 | 11 | public MemberAlreadyInChannelException() { 12 | super(ExceptionConstant.Messages.MEMBER_ALREADY_EXIST.getValue(), MemberAlreadyInChannelException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.Member.ALREADY_EXIST.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/member/MemberInChannelNotFoundException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.member; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.NOT_FOUND) 9 | public class MemberInChannelNotFoundException extends BaseException { 10 | 11 | public MemberInChannelNotFoundException() { 12 | super(ExceptionConstant.Messages.MEMBER_NOT_FOUND.getValue(), MemberInChannelNotFoundException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.Member.NOT_FOUND.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/member/MissingPermissionsException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.member; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.FORBIDDEN) 9 | public class MissingPermissionsException extends BaseException { 10 | 11 | public MissingPermissionsException() { 12 | super(ExceptionConstant.Messages.MISSING_PERMISSIONS.getValue(), MissingPermissionsException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.Member.MISSING_PERMISSIONS.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/message/AttachmentsCannotBeEmpty.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.message; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.BAD_REQUEST) 9 | public class AttachmentsCannotBeEmpty extends BaseException { 10 | 11 | public AttachmentsCannotBeEmpty() { 12 | super(ExceptionConstant.Messages.ATTACHMENTS_CANNOT_BE_EMPTY.getValue(), AttachmentsCannotBeEmpty.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.Message.ATTACHMENTS_CANNOT_BE_EMPTY.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/message/MessageCannotBeEmpty.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.message; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.BAD_REQUEST) 9 | public class MessageCannotBeEmpty extends BaseException { 10 | 11 | public MessageCannotBeEmpty() { 12 | super(ExceptionConstant.Messages.MESSAGE_CANNOT_BE_EMPTY.getValue(), MessageCannotBeEmpty.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.Message.CANNOT_BE_EMPTY.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/message/MessageNotFoundException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.message; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.NOT_FOUND) 9 | public class MessageNotFoundException extends BaseException { 10 | 11 | public MessageNotFoundException() { 12 | super(ExceptionConstant.Messages.MESSAGE_NOT_FOUND.getValue(), MessageNotFoundException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.Message.NOT_FOUND.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/message/UnknownAttachmentsException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.message; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.BAD_REQUEST) 9 | public class UnknownAttachmentsException extends BaseException { 10 | 11 | public UnknownAttachmentsException() { 12 | super(ExceptionConstant.Messages.UNKNOWN_ATTACHMENTS.getValue(), UnknownAttachmentsException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.Message.UNKNOWN_ATTACHMENTS.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/otp/NeedToWaitBeforeResendException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.otp; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.NOT_FOUND) 9 | public class NeedToWaitBeforeResendException extends BaseException { 10 | 11 | public NeedToWaitBeforeResendException() { 12 | super(ExceptionConstant.Messages.NEED_TO_WAIT.getValue(), NeedToWaitBeforeResendException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.OTP.WAIT_TO_RESEND.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/otp/OTPExpiredException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.otp; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.NOT_FOUND) 9 | public class OTPExpiredException extends BaseException { 10 | 11 | public OTPExpiredException() { 12 | super(ExceptionConstant.Messages.OTP_EXPIRED.getValue(), OTPExpiredException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.OTP.EXPIRED.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/otp/OTPsInvalidException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.otp; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.NOT_FOUND) 9 | public class OTPsInvalidException extends BaseException { 10 | 11 | public OTPsInvalidException() { 12 | super(ExceptionConstant.Messages.OTP_IS_INVALID.getValue(), OTPsInvalidException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.OTP.IS_INVALID.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/user/UserContactAlreadyExistException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.user; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.FORBIDDEN) 9 | public class UserContactAlreadyExistException extends BaseException { 10 | 11 | public UserContactAlreadyExistException() { 12 | super(ExceptionConstant.Messages.USER_CONTACT_ALREADY_EXIST.getValue(), UserContactAlreadyExistException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.User.CONTACT_ALREADY_EXIST.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/user/UserContactNotFoundException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.user; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.FORBIDDEN) 9 | public class UserContactNotFoundException extends BaseException { 10 | 11 | public UserContactNotFoundException() { 12 | super(ExceptionConstant.Messages.USER_CONTACT_NOT_FOUND.getValue(), UserContactNotFoundException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.User.CONTACT_NOT_FOUND.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/user/UserCredentialsDuplicateException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.user; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.FORBIDDEN) 9 | public class UserCredentialsDuplicateException extends BaseException { 10 | 11 | public UserCredentialsDuplicateException() { 12 | super(ExceptionConstant.Messages.USER_CREDENTIALS_DUPLICATE.getValue(), UserCredentialsDuplicateException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.User.CREDENTIALS_DUPLICATE.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/user/UserCredentialsIsInvalidException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.user; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.BAD_REQUEST) 9 | public class UserCredentialsIsInvalidException extends BaseException { 10 | 11 | public UserCredentialsIsInvalidException() { 12 | super(ExceptionConstant.Messages.USER_CREDENTIALS_IS_INVALID.getValue(), UserCredentialsIsInvalidException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.User.CREDENTIALS_IS_INVALID.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/user/UserEmailNotVerifiedException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.user; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.FORBIDDEN) 9 | public class UserEmailNotVerifiedException extends BaseException { 10 | 11 | public UserEmailNotVerifiedException() { 12 | super(ExceptionConstant.Messages.USER_EMAIL_VERIFIED.getValue(), UserEmailNotVerifiedException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.User.EMAIL_NOT_VERIFIED.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/user/UserNotFoundException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.user; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.NOT_FOUND) 9 | public class UserNotFoundException extends BaseException { 10 | 11 | public UserNotFoundException() { 12 | super(ExceptionConstant.Messages.USER_NOT_FOUND.getValue(), UserNotFoundException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.User.NOT_FOUND.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/exception/user/UserUnauthorizedException.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.exception.user; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | import su.foxochat.constant.ExceptionConstant; 6 | import su.foxochat.exception.BaseException; 7 | 8 | @ResponseStatus(HttpStatus.UNAUTHORIZED) 9 | public class UserUnauthorizedException extends BaseException { 10 | 11 | public UserUnauthorizedException() { 12 | super(ExceptionConstant.Messages.USER_UNAUTHORIZED.getValue(), UserUnauthorizedException.class.getAnnotation(ResponseStatus.class).value(), ExceptionConstant.User.UNAUTHORIZED.getValue()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/handler/HeartbeatHandler.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.handler; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.socket.TextMessage; 7 | import org.springframework.web.socket.WebSocketSession; 8 | import su.foxochat.constant.GatewayConstant; 9 | import su.foxochat.dto.gateway.EventDTO; 10 | import su.foxochat.dto.gateway.response.HeartbeatACKDTO; 11 | import su.foxochat.handler.structure.BaseHandler; 12 | import su.foxochat.model.Session; 13 | 14 | import java.io.IOException; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | 17 | @Slf4j 18 | @Component 19 | public class HeartbeatHandler implements BaseHandler { 20 | 21 | private final ObjectMapper objectMapper; 22 | 23 | public HeartbeatHandler(ObjectMapper objectMapper) { 24 | this.objectMapper = objectMapper; 25 | } 26 | 27 | @Override 28 | public int getOpcode() { 29 | return GatewayConstant.Opcode.HEARTBEAT.ordinal(); 30 | } 31 | 32 | @Override 33 | public void handle(WebSocketSession session, ConcurrentHashMap sessions, EventDTO payload) throws IOException { 34 | Session userSession = sessions.get(session.getId()); 35 | 36 | if (userSession.isAuthenticated()) return; 37 | 38 | userSession.setLastPingTimestamp(System.currentTimeMillis()); 39 | 40 | session.sendMessage(new TextMessage(objectMapper.writeValueAsString(new HeartbeatACKDTO()))); 41 | log.debug("Got heartbeat from session ({})", session.getId()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/handler/HelloHandler.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.handler; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.socket.TextMessage; 7 | import org.springframework.web.socket.WebSocketSession; 8 | import su.foxochat.constant.GatewayConstant; 9 | import su.foxochat.constant.UserConstant; 10 | import su.foxochat.dto.gateway.EventDTO; 11 | import su.foxochat.dto.gateway.response.HelloDTO; 12 | import su.foxochat.handler.structure.BaseHandler; 13 | import su.foxochat.model.Session; 14 | import su.foxochat.service.AuthenticationService; 15 | import su.foxochat.service.UserService; 16 | 17 | import java.util.Map; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | 20 | @Slf4j 21 | @Component 22 | public class HelloHandler implements BaseHandler { 23 | 24 | private final AuthenticationService authenticationService; 25 | 26 | private final ObjectMapper objectMapper; 27 | 28 | private final UserService userService; 29 | 30 | public HelloHandler(AuthenticationService authenticationService, ObjectMapper objectMapper, UserService userService) { 31 | this.authenticationService = authenticationService; 32 | this.objectMapper = objectMapper; 33 | this.userService = userService; 34 | } 35 | 36 | @Override 37 | public int getOpcode() { 38 | return GatewayConstant.Opcode.IDENTIFY.ordinal(); 39 | } 40 | 41 | @Override 42 | public void handle(WebSocketSession session, ConcurrentHashMap sessions, EventDTO payload) throws Exception { 43 | @SuppressWarnings("unchecked") 44 | String accessToken = ((Map) payload.getD()).get("token"); 45 | 46 | long userId = authenticationService.getUser(accessToken, true, false).getId(); 47 | Session userSession = sessions.get(session.getId()); 48 | userSession.setUserId(userId); 49 | userSession.setLastPingTimestamp(System.currentTimeMillis()); 50 | 51 | session.sendMessage(new TextMessage(objectMapper.writeValueAsString(new HelloDTO()))); 52 | userService.setStatus(userId, UserConstant.Status.ONLINE.getStatus()); 53 | log.debug("Authenticated session ({}) with user id {}", session.getId(), userId); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/handler/TypingStartHandler.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.handler; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.context.annotation.Lazy; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.socket.WebSocketSession; 8 | import su.foxochat.constant.GatewayConstant; 9 | import su.foxochat.dto.gateway.EventDTO; 10 | import su.foxochat.dto.gateway.response.TypingStartDTO; 11 | import su.foxochat.handler.structure.BaseHandler; 12 | import su.foxochat.model.Member; 13 | import su.foxochat.model.Session; 14 | import su.foxochat.service.ChannelService; 15 | import su.foxochat.service.GatewayService; 16 | 17 | import java.util.List; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | 20 | @Slf4j 21 | @Component 22 | public class TypingStartHandler implements BaseHandler { 23 | 24 | private final GatewayService gatewayService; 25 | 26 | private final ObjectMapper objectMapper; 27 | 28 | private final ChannelService channelService; 29 | 30 | public TypingStartHandler(@Lazy GatewayService gatewayService, ObjectMapper objectMapper, ChannelService channelService) { 31 | this.gatewayService = gatewayService; 32 | this.objectMapper = objectMapper; 33 | this.channelService = channelService; 34 | } 35 | 36 | @Override 37 | public int getOpcode() { 38 | return GatewayConstant.Opcode.DISPATCH.ordinal(); 39 | } 40 | 41 | @Override 42 | public void handle(WebSocketSession session, ConcurrentHashMap sessions, EventDTO payload) throws Exception { 43 | TypingStartDTO data = objectMapper.convertValue(payload.getD(), TypingStartDTO.class); 44 | long channelId = data.getD().get("channelId"); 45 | Session userSession = sessions.get(session.getId()); 46 | 47 | if (!userSession.isAuthenticated()) return; 48 | 49 | List recipients = channelService.getById(channelId).getMembers().stream().map(Member::getId).toList(); 50 | 51 | gatewayService.sendMessageToSpecificSessions(recipients, GatewayConstant.Opcode.DISPATCH.ordinal(), new TypingStartDTO(channelId, userSession.getUserId(), System.currentTimeMillis()), GatewayConstant.Event.TYPING_START.getValue()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/handler/structure/BaseHandler.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.handler.structure; 2 | 3 | import org.springframework.stereotype.Component; 4 | import org.springframework.web.socket.WebSocketSession; 5 | import su.foxochat.dto.gateway.EventDTO; 6 | import su.foxochat.model.Session; 7 | 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | @Component 11 | public interface BaseHandler { 12 | 13 | int getOpcode(); 14 | 15 | void handle(WebSocketSession session, ConcurrentHashMap sessions, EventDTO payload) throws Exception; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/handler/structure/EventHandler.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.handler.structure; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import lombok.Getter; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.lang.NonNull; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.web.socket.CloseStatus; 9 | import org.springframework.web.socket.TextMessage; 10 | import org.springframework.web.socket.WebSocketSession; 11 | import org.springframework.web.socket.handler.TextWebSocketHandler; 12 | import su.foxochat.constant.CloseCodeConstant; 13 | import su.foxochat.constant.ExceptionConstant; 14 | import su.foxochat.constant.GatewayConstant; 15 | import su.foxochat.constant.UserConstant; 16 | import su.foxochat.dto.gateway.EventDTO; 17 | import su.foxochat.exception.user.UserUnauthorizedException; 18 | import su.foxochat.model.Session; 19 | import su.foxochat.service.UserService; 20 | 21 | import java.io.IOException; 22 | import java.util.concurrent.ConcurrentHashMap; 23 | import java.util.concurrent.Executors; 24 | import java.util.concurrent.ScheduledExecutorService; 25 | import java.util.concurrent.TimeUnit; 26 | 27 | @Slf4j 28 | @Component 29 | public class EventHandler extends TextWebSocketHandler { 30 | 31 | private final EventHandlerRegistry handlerRegistry; 32 | 33 | private final ObjectMapper objectMapper; 34 | 35 | @Getter 36 | private final ConcurrentHashMap sessions = new ConcurrentHashMap<>(); 37 | 38 | private final UserService userService; 39 | 40 | public EventHandler(EventHandlerRegistry handlerRegistry, ObjectMapper objectMapper, UserService userService) { 41 | this.handlerRegistry = handlerRegistry; 42 | this.objectMapper = objectMapper; 43 | this.userService = userService; 44 | 45 | try (ScheduledExecutorService executor = Executors.newScheduledThreadPool(1, Thread.ofVirtual().factory())) { 46 | Runnable task = () -> sessions.values().forEach(session -> { 47 | long lastPingTimestamp = session.getLastPingTimestamp(); 48 | 49 | long timeout = (GatewayConstant.HEARTBEAT_INTERVAL + GatewayConstant.HEARTBEAT_TIMEOUT); 50 | 51 | if (lastPingTimestamp < (System.currentTimeMillis() - timeout)) { 52 | try { 53 | session.getWebSocketSession().close(CloseCodeConstant.HEARTBEAT_TIMEOUT); 54 | log.debug("Session closed due to heartbeat timeout: {}", session.getWebSocketSession().getId()); 55 | } catch (IOException e) { 56 | log.error("Error closing session: {}", session.getWebSocketSession().getId(), e); 57 | throw new RuntimeException(e); 58 | } 59 | } 60 | }); 61 | 62 | executor.scheduleAtFixedRate(task, 0, 30, TimeUnit.SECONDS); 63 | } catch (Exception e) { 64 | log.error("Error initializing thread: {}", e.getMessage()); 65 | } 66 | } 67 | 68 | @Override 69 | public void afterConnectionEstablished(@NonNull WebSocketSession session) { 70 | log.debug("Connection for session ({}) established", session.getId()); 71 | sessions.put(session.getId(), new Session(session)); 72 | } 73 | 74 | @Override 75 | public void afterConnectionClosed(WebSocketSession session, @NonNull CloseStatus status) throws Exception { 76 | log.debug("Connection for session ({}) closed with status {} ({})", session.getId(), status.getReason(), status.getCode()); 77 | 78 | Session userSession = sessions.get(session.getId()); 79 | 80 | if (userSession.isAuthenticated()) { 81 | userService.setStatus(userSession.getUserId(), UserConstant.Status.ONLINE.getStatus()); 82 | } 83 | sessions.remove(session.getId()); 84 | } 85 | 86 | @Override 87 | protected void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) throws Exception { 88 | try { 89 | EventDTO payload = objectMapper.readValue(message.getPayload(), EventDTO.class); 90 | int opcode = payload.getOp(); 91 | 92 | BaseHandler handler = handlerRegistry.getHandler(opcode); 93 | 94 | if (handler != null) { 95 | handler.handle(session, sessions, payload); 96 | log.debug("Handling {} event with opcode {}", payload.getT(), payload.getOp()); 97 | } 98 | } catch (UserUnauthorizedException e) { 99 | log.error(ExceptionConstant.Messages.SERVER_EXCEPTION.getValue(), null, null, message); 100 | 101 | session.close(CloseCodeConstant.UNAUTHORIZED); 102 | } catch (Exception e) { 103 | log.error(ExceptionConstant.Messages.SERVER_EXCEPTION.getValue(), null, null, message); 104 | log.error(ExceptionConstant.Messages.SERVER_EXCEPTION_STACKTRACE.getValue(), e); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/handler/structure/EventHandlerRegistry.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.handler.structure; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | @Component 10 | public class EventHandlerRegistry { 11 | 12 | private final Map handlers = new HashMap<>(); 13 | 14 | public EventHandlerRegistry(List handlers) { 15 | for (BaseHandler handler : handlers) { 16 | this.handlers.put(handler.getOpcode(), handler); 17 | } 18 | } 19 | 20 | public BaseHandler getHandler(int event) { 21 | return handlers.get(event); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/interceptor/AuthenticationInterceptor.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.interceptor; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.HttpHeaders; 7 | import org.springframework.http.HttpMethod; 8 | import org.springframework.lang.NonNull; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.servlet.HandlerInterceptor; 11 | import su.foxochat.config.APIConfig; 12 | import su.foxochat.constant.AttributeConstant; 13 | import su.foxochat.exception.user.UserEmailNotVerifiedException; 14 | import su.foxochat.exception.user.UserUnauthorizedException; 15 | import su.foxochat.model.User; 16 | import su.foxochat.service.AuthenticationService; 17 | 18 | import java.util.Objects; 19 | import java.util.Set; 20 | 21 | @Slf4j 22 | @Component 23 | public class AuthenticationInterceptor implements HandlerInterceptor { 24 | 25 | private static final Set EMAIL_VERIFICATION_IGNORE_PATHS = Set.of( 26 | "/auth/email/verify", 27 | "/users/@me", 28 | "/auth/email/resend" 29 | ); 30 | 31 | final AuthenticationService authenticationService; 32 | 33 | final APIConfig apiConfig; 34 | 35 | public AuthenticationInterceptor(AuthenticationService authenticationService, APIConfig apiConfig) { 36 | this.authenticationService = authenticationService; 37 | this.apiConfig = apiConfig; 38 | } 39 | 40 | @Override 41 | public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws UserUnauthorizedException, UserEmailNotVerifiedException { 42 | if (Objects.equals(request.getMethod(), HttpMethod.OPTIONS.name())) return true; 43 | 44 | String requestURI = request.getRequestURI(); 45 | boolean ignoreEmailVerification = EMAIL_VERIFICATION_IGNORE_PATHS.stream().anyMatch(requestURI::contains); 46 | if (apiConfig.isDevelopment()) ignoreEmailVerification = true; 47 | 48 | String accessToken = request.getHeader(HttpHeaders.AUTHORIZATION); 49 | 50 | User user = authenticationService.authUser(accessToken, ignoreEmailVerification); 51 | 52 | request.setAttribute(AttributeConstant.USER, user); 53 | request.setAttribute(AttributeConstant.ACCESS_TOKEN, accessToken); 54 | 55 | log.debug("Authenticated user {} successfully", user.getUsername()); 56 | return true; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/interceptor/ChannelInterceptor.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.interceptor; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.HttpMethod; 7 | import org.springframework.lang.NonNull; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.servlet.HandlerInterceptor; 10 | import su.foxochat.constant.AttributeConstant; 11 | import su.foxochat.constant.ChannelConstant; 12 | import su.foxochat.exception.channel.ChannelNotFoundException; 13 | import su.foxochat.model.Channel; 14 | import su.foxochat.model.User; 15 | import su.foxochat.service.ChannelService; 16 | 17 | import java.util.regex.Matcher; 18 | import java.util.regex.Pattern; 19 | 20 | @Slf4j 21 | @Component 22 | public class ChannelInterceptor implements HandlerInterceptor { 23 | 24 | private final ChannelService channelService; 25 | 26 | private static final Pattern CHANNEL_ID_PATTERN = Pattern.compile("/channels/(\\d+)"); 27 | 28 | public ChannelInterceptor(ChannelService channelService) { 29 | this.channelService = channelService; 30 | } 31 | 32 | @Override 33 | public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws ChannelNotFoundException { 34 | if (HttpMethod.OPTIONS.matches(request.getMethod())) return true; 35 | 36 | String uri = request.getRequestURI(); 37 | Matcher matcher = CHANNEL_ID_PATTERN.matcher(uri); 38 | 39 | if (!matcher.find()) { 40 | throw new ChannelNotFoundException(); 41 | } 42 | 43 | long id = Long.parseLong(matcher.group(1)); 44 | Channel channel = channelService.getById(id); 45 | 46 | User user = (User) request.getAttribute(AttributeConstant.USER); 47 | 48 | if (!channel.hasFlag(ChannelConstant.Flags.PUBLIC) && channel.getMembers().stream().noneMatch(u -> u.getId() == user.getId())) { 49 | throw new ChannelNotFoundException(); 50 | } 51 | 52 | request.setAttribute(AttributeConstant.CHANNEL, channel); 53 | 54 | log.debug("Got channel {} successfully", channel.getId()); 55 | return true; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/interceptor/MemberInterceptor.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.interceptor; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.HttpMethod; 7 | import org.springframework.lang.NonNull; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.servlet.HandlerInterceptor; 10 | import su.foxochat.constant.AttributeConstant; 11 | import su.foxochat.exception.channel.ChannelNotFoundException; 12 | import su.foxochat.model.Channel; 13 | import su.foxochat.model.Member; 14 | import su.foxochat.model.User; 15 | import su.foxochat.service.MemberService; 16 | 17 | import java.util.Objects; 18 | 19 | @Slf4j 20 | @Component 21 | public class MemberInterceptor implements HandlerInterceptor { 22 | 23 | private final MemberService memberService; 24 | 25 | public MemberInterceptor(MemberService memberService) { 26 | this.memberService = memberService; 27 | } 28 | 29 | @Override 30 | public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws ChannelNotFoundException { 31 | if (Objects.equals(request.getMethod(), HttpMethod.OPTIONS.name())) return true; 32 | 33 | if (Objects.equals(request.getMethod(), HttpMethod.PUT.name()) && request.getRequestURI().matches("/channels/\\d+/members/@me")) { 34 | return true; 35 | } 36 | 37 | User user = (User) request.getAttribute(AttributeConstant.USER); 38 | Channel channel = (Channel) request.getAttribute(AttributeConstant.CHANNEL); 39 | 40 | Member member = memberService.getByChannelIdAndUserId(channel.getId(), user.getId()) 41 | .orElseThrow(ChannelNotFoundException::new); 42 | 43 | request.setAttribute(AttributeConstant.MEMBER, member); 44 | 45 | log.debug("Got member {} in channel {} successfully", member.getId(), channel.getId()); 46 | return true; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/model/Attachment.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Entity 8 | @Getter 9 | @Setter 10 | @Table(name = "attachments", indexes = { 11 | @Index(name = "idx_attachment_id", columnList = "id", unique = true), 12 | @Index(name = "idx_attachment_user_id", columnList = "id, user_id") 13 | }) 14 | public class Attachment { 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | private long id; 19 | 20 | @ManyToOne 21 | @JoinColumn(name = "user_id", nullable = false) 22 | private User user; 23 | 24 | @Column 25 | private String uuid; 26 | 27 | @Column 28 | private String filename; 29 | 30 | @Column 31 | private String contentType; 32 | 33 | @Column 34 | private long flags; 35 | 36 | public Attachment() {} 37 | 38 | public Attachment(User user, String uuid, String filename, String contentType, long flags, boolean includeUser) { 39 | if (includeUser) this.user = user; 40 | this.uuid = uuid; 41 | this.filename = filename; 42 | this.contentType = contentType; 43 | this.flags = flags; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/model/Channel.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import su.foxochat.constant.ChannelConstant; 7 | 8 | import java.util.List; 9 | 10 | @Getter 11 | @Setter 12 | @Entity 13 | @Table(name = "channels", indexes = { 14 | @Index(name = "idx_channel_id", columnList = "id", unique = true), 15 | @Index(name = "idx_channel_name", columnList = "name", unique = true) 16 | }) 17 | public class Channel { 18 | 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | public long id; 22 | 23 | @Column 24 | public String displayName; 25 | 26 | @Column 27 | public String name; 28 | 29 | @JoinColumn(name = "icon_id") 30 | @ManyToOne(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY) 31 | public Attachment icon; 32 | 33 | @Column 34 | public int type; 35 | 36 | @Column 37 | public long flags; 38 | 39 | @ManyToOne 40 | @JoinColumn(name = "user_id", nullable = false) 41 | public User owner; 42 | 43 | @OneToMany(mappedBy = "channel", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) 44 | private List members; 45 | 46 | @OneToMany(mappedBy = "channel", cascade = CascadeType.REMOVE, orphanRemoval = true, fetch = FetchType.EAGER) 47 | private List messages; 48 | 49 | @Column 50 | public long createdAt; 51 | 52 | public Channel() {} 53 | 54 | public Channel(String displayName, String name, long flags, int type, User owner) { 55 | this.displayName = displayName; 56 | this.name = name.toLowerCase(); 57 | this.type = type; 58 | this.owner = owner; 59 | this.flags = flags; 60 | this.createdAt = System.currentTimeMillis(); 61 | } 62 | 63 | public void addFlag(ChannelConstant.Flags flag) { 64 | this.flags |= flag.getBit(); 65 | } 66 | 67 | public void removeFlag(ChannelConstant.Flags flag) { 68 | this.flags &= ~flag.getBit(); 69 | } 70 | 71 | public boolean hasFlag(ChannelConstant.Flags flag) { 72 | return (this.flags & flag.getBit()) != 0; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/model/Member.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import su.foxochat.constant.MemberConstant; 7 | import su.foxochat.exception.member.MissingPermissionsException; 8 | 9 | @Setter 10 | @Getter 11 | @Entity 12 | @Table(name = "members", indexes = { 13 | @Index(name = "idx_member_user_channel", columnList = "user_id, channel_id") 14 | }) 15 | public class Member { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | public long id; 20 | 21 | @Column 22 | public long permissions; 23 | 24 | @ManyToOne 25 | @JoinColumn(name = "user_id", nullable = false) 26 | private User user; 27 | 28 | @ManyToOne 29 | @JoinColumn(name = "channel_id", nullable = false) 30 | private Channel channel; 31 | 32 | @Column 33 | public long joinedAt; 34 | 35 | public Member() {} 36 | 37 | public Member(User user, Channel channel, long permissions) { 38 | this.user = user; 39 | this.channel = channel; 40 | this.permissions = permissions; 41 | this.joinedAt = System.currentTimeMillis(); 42 | } 43 | 44 | public void addPermission(MemberConstant.Permissions permission) { 45 | this.permissions |= permission.getBit(); 46 | } 47 | 48 | public void addPermissions(MemberConstant.Permissions... permissions) { 49 | for (MemberConstant.Permissions permission : permissions) { 50 | this.permissions |= permission.getBit(); 51 | } 52 | } 53 | 54 | public void setPermissions(MemberConstant.Permissions... permissions) { 55 | this.permissions = 0; 56 | for (MemberConstant.Permissions permission : permissions) { 57 | this.permissions |= permission.getBit(); 58 | } 59 | } 60 | 61 | public void removePermission(MemberConstant.Permissions permission) { 62 | this.permissions &= ~permission.getBit(); 63 | } 64 | 65 | public boolean hasPermission(MemberConstant.Permissions permission) { 66 | return (this.permissions & permission.getBit()) != 0; 67 | } 68 | 69 | public void hasPermissions(MemberConstant.Permissions... permissions) throws MissingPermissionsException { 70 | for (MemberConstant.Permissions permission : permissions) { 71 | if ((this.permissions & permission.getBit()) == 0) { 72 | return; 73 | } 74 | } 75 | 76 | throw new MissingPermissionsException(); 77 | } 78 | 79 | public boolean hasAnyPermission(MemberConstant.Permissions... permissions) { 80 | for (MemberConstant.Permissions permission : permissions) { 81 | if ((this.permissions & permission.getBit()) != 0) { 82 | return true; 83 | } 84 | } 85 | 86 | return false; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/model/Message.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | 10 | @Setter 11 | @Getter 12 | @Entity 13 | @Table(name = "messages", indexes = { 14 | @Index(name = "idx_message_id_channel_id", columnList = "id, channel_id") 15 | }) 16 | public class Message { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | public long id; 21 | 22 | @Column(columnDefinition = "TEXT") 23 | public String content; 24 | 25 | @ManyToOne 26 | @JoinColumn(name = "author", nullable = false) 27 | public Member author; 28 | 29 | @Column 30 | public long timestamp; 31 | 32 | @OneToMany(mappedBy = "message", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) 33 | public List attachments; 34 | 35 | @ManyToOne 36 | @JoinColumn(name = "channel_id", nullable = false) 37 | private Channel channel; 38 | 39 | public Message() {} 40 | 41 | public Message(Channel channel, String content, Member member, List attachments) { 42 | this.channel = channel; 43 | this.author = member; 44 | this.content = content; 45 | this.timestamp = System.currentTimeMillis(); 46 | this.attachments = attachments.stream() 47 | .map(attachment -> new MessageAttachment(this, attachment)) 48 | .collect(Collectors.toList()); 49 | } 50 | 51 | public boolean isAuthor(Member member) { 52 | return author.getUser().getUsername().equals(member.getUser().getUsername()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/model/MessageAttachment.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Setter 8 | @Getter 9 | @Entity 10 | @Table(name = "message_attachments", indexes = { 11 | @Index(name = "idx_message_attachment", columnList = "message_id, attachment_id") 12 | }) 13 | public class MessageAttachment { 14 | 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.IDENTITY) 17 | private long id; 18 | 19 | @ManyToOne(fetch = FetchType.LAZY) 20 | @JoinColumn(name = "message_id", nullable = false) 21 | private Message message; 22 | 23 | @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) 24 | @JoinColumn(name = "attachment_id", nullable = false, unique = true) 25 | private Attachment attachment; 26 | 27 | public MessageAttachment() {} 28 | 29 | public MessageAttachment(Message message, Attachment attachment) { 30 | this.message = message; 31 | this.attachment = attachment; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/model/OTP.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Getter 8 | @Setter 9 | @Entity 10 | @Table(name = "otps", indexes = { 11 | @Index(name = "idx_otp_user_id", columnList = "userId", unique = true), 12 | @Index(name = "idx_otp_value", columnList = "value", unique = true) 13 | }) 14 | public class OTP { 15 | 16 | @Id 17 | public long userId; 18 | 19 | @Column 20 | public String type; 21 | 22 | @Column 23 | public String value; 24 | 25 | @Column 26 | public long issuedAt; 27 | 28 | @Column 29 | public long expiresAt; 30 | 31 | public OTP() {} 32 | 33 | public OTP(long userId, String type, String value, long issuedAt, long expiresAt) { 34 | this.userId = userId; 35 | this.type = type; 36 | this.value = value; 37 | this.issuedAt = issuedAt; 38 | this.expiresAt = expiresAt; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/model/Session.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.web.socket.WebSocketSession; 6 | 7 | @Getter 8 | @Setter 9 | public class Session { 10 | 11 | private long userId; 12 | 13 | private long lastPingTimestamp; 14 | 15 | private int sequence; 16 | 17 | private WebSocketSession webSocketSession; 18 | 19 | public Session(WebSocketSession webSocketSession) { 20 | this.lastPingTimestamp = System.currentTimeMillis(); 21 | this.webSocketSession = webSocketSession; 22 | } 23 | 24 | public boolean isAuthenticated() { 25 | return userId != 0; 26 | } 27 | 28 | public void increaseSequence() { 29 | this.sequence++; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/model/User.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import su.foxochat.constant.UserConstant; 7 | 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | 11 | @Setter 12 | @Getter 13 | @Entity 14 | @Table(name = "users", indexes = { 15 | @Index(name = "idx_user_id", columnList = "id"), 16 | @Index(name = "idx_user_username", columnList = "username", unique = true), 17 | @Index(name = "idx_user_email", columnList = "email", unique = true) 18 | }) 19 | public class User { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | public long id; 24 | 25 | @Column 26 | public String displayName; 27 | 28 | @Column 29 | public String username; 30 | 31 | @Column 32 | private String email; 33 | 34 | @OneToMany(mappedBy = "contact", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) 35 | private List contacts; 36 | 37 | @Column 38 | private int status; 39 | 40 | @Column 41 | private long statusUpdatedAt; 42 | 43 | @Column 44 | private String password; 45 | 46 | @Column(nullable = false) 47 | private int tokenVersion; 48 | 49 | @JoinColumn(name = "avatar_id") 50 | @ManyToOne(cascade = CascadeType.REMOVE) 51 | public Attachment avatar; 52 | 53 | @Column 54 | public long flags; 55 | 56 | @Column 57 | public int type; 58 | 59 | @Column 60 | private long createdAt; 61 | 62 | @Column 63 | private long deletedAt; 64 | 65 | public User() {} 66 | 67 | public User(String username, String email, String password, long flags, int type) { 68 | this.displayName = null; 69 | this.username = username.toLowerCase(); 70 | this.email = email; 71 | this.password = password; 72 | if (this.contacts != null) this.contacts = contacts.stream() 73 | .map(userContact -> new UserContact(this, userContact.getContact())) 74 | .collect(Collectors.toList()); 75 | this.status = UserConstant.Status.OFFLINE.getStatus(); 76 | this.statusUpdatedAt = System.currentTimeMillis(); 77 | this.flags = flags; 78 | this.type = type; 79 | this.createdAt = System.currentTimeMillis(); 80 | } 81 | 82 | public void addFlag(UserConstant.Flags flag) { 83 | this.flags |= flag.getBit(); 84 | } 85 | 86 | public void removeFlag(UserConstant.Flags flag) { 87 | this.flags &= ~flag.getBit(); 88 | } 89 | 90 | public boolean hasFlag(UserConstant.Flags flag) { 91 | return (this.flags & flag.getBit()) != 0; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/model/UserContact.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.model; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | @Setter 8 | @Getter 9 | @Entity 10 | @Table(name = "user_contacts", indexes = { 11 | @Index(name = "idx_user_contact", columnList = "user_id, contact_id", unique = true) 12 | }) 13 | public class UserContact { 14 | 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.IDENTITY) 17 | private long id; 18 | 19 | @ManyToOne(fetch = FetchType.LAZY) 20 | @JoinColumn(name = "user_id", nullable = false) 21 | private User user; 22 | 23 | @ManyToOne(fetch = FetchType.LAZY) 24 | @JoinColumn(name = "contact_id", nullable = false) 25 | private User contact; 26 | 27 | public UserContact() { 28 | } 29 | 30 | public UserContact(User user, User contact) { 31 | this.user = user; 32 | this.contact = contact; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/repository/AttachmentRepository.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.repository; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.stereotype.Repository; 5 | import su.foxochat.model.Attachment; 6 | 7 | import java.util.Optional; 8 | 9 | @Repository 10 | public interface AttachmentRepository extends CrudRepository { 11 | 12 | Optional findById(long id); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/repository/ChannelRepository.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.repository; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.stereotype.Repository; 5 | import su.foxochat.model.Channel; 6 | 7 | import java.util.Optional; 8 | 9 | @Repository 10 | public interface ChannelRepository extends CrudRepository { 11 | 12 | Optional findById(long id); 13 | 14 | Optional findByName(String name); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/repository/MemberRepository.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.repository; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.stereotype.Repository; 5 | import su.foxochat.model.Member; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | @Repository 11 | public interface MemberRepository extends CrudRepository { 12 | 13 | Optional findByChannelIdAndUserId(long channelId, long userId); 14 | 15 | List findAllByUserId(long userId); 16 | 17 | List findAllByChannelId(long channelId); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/repository/MessageRepository.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.repository; 2 | 3 | import org.springframework.data.jpa.repository.Query; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.data.repository.query.Param; 6 | import org.springframework.lang.NonNull; 7 | import org.springframework.stereotype.Repository; 8 | import su.foxochat.model.Channel; 9 | import su.foxochat.model.Message; 10 | 11 | import java.util.List; 12 | import java.util.Optional; 13 | 14 | @Repository 15 | public interface MessageRepository extends CrudRepository { 16 | 17 | @Query("SELECT m FROM Message m WHERE m.channel = :ch AND m.timestamp < :before ORDER BY m.id DESC LIMIT :limit") 18 | List findAllByChannel(@Param("ch") Channel channel, @Param("before") long before, @Param("limit") int limit); 19 | 20 | @Query("SELECT m FROM Message m WHERE m.channel = :ch AND m.id = :id") 21 | Optional findByChannelAndId(@Param("ch") Channel channel, @Param("id") long id); 22 | 23 | @Query("SELECT m FROM Message m WHERE m.channel = :ch ORDER BY m.id DESC LIMIT 1") 24 | Optional getLastMessageByChannel(@Param("ch") Channel channel); 25 | 26 | @NonNull 27 | List findAll(); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/repository/OTPRepository.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.repository; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.stereotype.Repository; 5 | import su.foxochat.model.OTP; 6 | 7 | import java.util.Optional; 8 | 9 | @Repository 10 | public interface OTPRepository extends CrudRepository { 11 | 12 | Optional findByUserId(long userId); 13 | 14 | Optional findByValue(String value); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.repository; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.stereotype.Repository; 5 | import su.foxochat.model.User; 6 | 7 | import java.util.Optional; 8 | 9 | @Repository 10 | public interface UserRepository extends CrudRepository { 11 | 12 | Optional findById(long id); 13 | 14 | Optional findByUsername(String username); 15 | 16 | Optional findByEmail(String email); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/AttachmentService.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service; 2 | 3 | import su.foxochat.dto.api.request.AttachmentAddDTO; 4 | import su.foxochat.dto.api.response.UploadAttachmentDTO; 5 | import su.foxochat.dto.internal.AttachmentPresignedDTO; 6 | import su.foxochat.exception.message.AttachmentsCannotBeEmpty; 7 | import su.foxochat.exception.message.UnknownAttachmentsException; 8 | import su.foxochat.model.Attachment; 9 | import su.foxochat.model.User; 10 | 11 | import java.util.List; 12 | 13 | public interface AttachmentService { 14 | 15 | AttachmentPresignedDTO getPresignedURLAndSave(AttachmentAddDTO attachment, User user); 16 | 17 | List uploadAll(User user, List attachments); 18 | 19 | AttachmentPresignedDTO upload(User user, AttachmentAddDTO attachment) throws UnknownAttachmentsException, AttachmentsCannotBeEmpty; 20 | 21 | List get(User user, List attachmentsIds) throws UnknownAttachmentsException; 22 | 23 | Attachment getById(long id) throws UnknownAttachmentsException; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/AuthenticationService.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service; 2 | 3 | import su.foxochat.dto.api.request.UserResetPasswordConfirmDTO; 4 | import su.foxochat.dto.api.request.UserResetPasswordDTO; 5 | import su.foxochat.exception.otp.NeedToWaitBeforeResendException; 6 | import su.foxochat.exception.otp.OTPExpiredException; 7 | import su.foxochat.exception.otp.OTPsInvalidException; 8 | import su.foxochat.exception.user.UserCredentialsDuplicateException; 9 | import su.foxochat.exception.user.UserCredentialsIsInvalidException; 10 | import su.foxochat.exception.user.UserEmailNotVerifiedException; 11 | import su.foxochat.exception.user.UserUnauthorizedException; 12 | import su.foxochat.model.User; 13 | 14 | public interface AuthenticationService { 15 | 16 | User getUser(String token, boolean ignoreEmailVerification, boolean removeBearerFromString) throws UserUnauthorizedException, UserEmailNotVerifiedException; 17 | 18 | String register(String username, String email, String password) throws UserCredentialsDuplicateException; 19 | 20 | String login(String email, String password) throws UserCredentialsIsInvalidException; 21 | 22 | void verifyEmail(User user, String pathCode) throws OTPsInvalidException, OTPExpiredException; 23 | 24 | void resendEmail(User user, String accessToken) throws OTPsInvalidException, NeedToWaitBeforeResendException; 25 | 26 | void resetPassword(UserResetPasswordDTO body) throws UserCredentialsIsInvalidException; 27 | 28 | void confirmResetPassword(UserResetPasswordConfirmDTO body) throws OTPExpiredException, OTPsInvalidException, UserCredentialsIsInvalidException; 29 | 30 | User authUser(String accessToken, boolean ignoreEmailVerification) throws UserUnauthorizedException, UserEmailNotVerifiedException; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/ChannelService.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service; 2 | 3 | import su.foxochat.dto.api.request.ChannelCreateDTO; 4 | import su.foxochat.dto.api.request.ChannelEditDTO; 5 | import su.foxochat.exception.channel.ChannelAlreadyExistException; 6 | import su.foxochat.exception.channel.ChannelNotFoundException; 7 | import su.foxochat.model.Channel; 8 | import su.foxochat.model.Member; 9 | import su.foxochat.model.User; 10 | 11 | public interface ChannelService { 12 | Channel add(User user, ChannelCreateDTO body) throws ChannelAlreadyExistException; 13 | 14 | Channel getById(long id) throws ChannelNotFoundException; 15 | 16 | Channel getByName(String name) throws ChannelNotFoundException; 17 | 18 | Channel update(Member member, Channel channel, ChannelEditDTO body) throws Exception; 19 | 20 | void delete(Channel channel, User user) throws Exception; 21 | 22 | Member addMember(Channel channel, User user) throws Exception; 23 | 24 | void removeMember(Channel channel, User user) throws Exception; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/EmailService.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service; 2 | 3 | import org.springframework.scheduling.annotation.Async; 4 | 5 | public interface EmailService { 6 | @Async 7 | void send(String to, long id, String type, String username, String digitCode, long issuedAt, long expiresAt, String token); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/GatewayService.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service; 2 | 3 | import java.util.List; 4 | 5 | public interface GatewayService { 6 | 7 | void sendMessageToSpecificSessions(List userIds, int opcode, Object data, String type) throws Exception; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/JwtService.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service; 2 | 3 | import su.foxochat.model.User; 4 | 5 | import javax.crypto.SecretKey; 6 | 7 | public interface JwtService { 8 | String generate(User user); 9 | 10 | SecretKey getSigningKey(int tokenVersion); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/MemberService.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service; 2 | 3 | import su.foxochat.model.Channel; 4 | import su.foxochat.model.Member; 5 | 6 | import java.util.List; 7 | import java.util.Optional; 8 | 9 | public interface MemberService { 10 | List getChannelsByUserId(long userId); 11 | 12 | List getAllByChannelId(long channelId); 13 | 14 | Optional getByChannelIdAndUserId(long channelId, long userId); 15 | 16 | Member add(Member member); 17 | 18 | void delete(Member member); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/MessageService.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service; 2 | 3 | import su.foxochat.dto.api.request.AttachmentAddDTO; 4 | import su.foxochat.dto.api.request.MessageCreateDTO; 5 | import su.foxochat.dto.api.response.UploadAttachmentDTO; 6 | import su.foxochat.exception.member.MemberInChannelNotFoundException; 7 | import su.foxochat.exception.member.MissingPermissionsException; 8 | import su.foxochat.exception.message.AttachmentsCannotBeEmpty; 9 | import su.foxochat.exception.message.MessageNotFoundException; 10 | import su.foxochat.model.Channel; 11 | import su.foxochat.model.Member; 12 | import su.foxochat.model.Message; 13 | import su.foxochat.model.User; 14 | 15 | import java.util.List; 16 | 17 | public interface MessageService { 18 | 19 | List getAllByChannel(long before, int limit, Channel channel); 20 | 21 | Message getByIdAndChannel(long id, Channel channel) throws MessageNotFoundException; 22 | 23 | Message add(Channel channel, User user, MessageCreateDTO body) throws Exception; 24 | 25 | List addAttachments(Channel channel, User user, List attachments) throws MissingPermissionsException, AttachmentsCannotBeEmpty, MemberInChannelNotFoundException; 26 | 27 | void delete(long id, Member member, Channel channel) throws Exception; 28 | 29 | Message update(long id, Channel channel, Member member, MessageCreateDTO body) throws Exception; 30 | 31 | Message getLastByChannel(Channel channel); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/OTPService.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service; 2 | 3 | import su.foxochat.exception.otp.OTPExpiredException; 4 | import su.foxochat.exception.otp.OTPsInvalidException; 5 | import su.foxochat.model.OTP; 6 | 7 | public interface OTPService { 8 | 9 | OTP validate(String pathCode) throws OTPsInvalidException, OTPExpiredException; 10 | 11 | void delete(OTP OTP); 12 | 13 | void save(long id, String type, String digitCode, long issuedAt, long expiresAt); 14 | 15 | OTP getByUserId(long userId) throws OTPsInvalidException; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/StorageService.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service; 2 | 3 | import su.foxochat.dto.internal.AttachmentPresignedDTO; 4 | 5 | public interface StorageService { 6 | 7 | AttachmentPresignedDTO getPresignedUrl(String bucketName); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/UserService.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service; 2 | 3 | import su.foxochat.constant.UserConstant; 4 | import su.foxochat.dto.api.request.UserEditDTO; 5 | import su.foxochat.exception.otp.OTPExpiredException; 6 | import su.foxochat.exception.otp.OTPsInvalidException; 7 | import su.foxochat.exception.user.UserContactAlreadyExistException; 8 | import su.foxochat.exception.user.UserContactNotFoundException; 9 | import su.foxochat.exception.user.UserCredentialsDuplicateException; 10 | import su.foxochat.exception.user.UserCredentialsIsInvalidException; 11 | import su.foxochat.model.User; 12 | 13 | import java.util.Optional; 14 | 15 | public interface UserService { 16 | 17 | Optional getById(long id); 18 | 19 | Optional getByUsername(String username); 20 | 21 | Optional getByEmail(String email); 22 | 23 | void updateFlags(User user, UserConstant.Flags removeFlag, UserConstant.Flags addFlag); 24 | 25 | User add(String username, String email, String password) throws UserCredentialsDuplicateException; 26 | 27 | User update(User user, UserEditDTO body) throws Exception; 28 | 29 | void requestDelete(User user, String password) throws UserCredentialsIsInvalidException; 30 | 31 | void confirmDelete(User user, String pathCode) throws OTPsInvalidException, OTPExpiredException; 32 | 33 | void setStatus(long userId, int status) throws Exception; 34 | 35 | User addContact(User user, long id) throws UserContactAlreadyExistException; 36 | 37 | void deleteContact(User user, long id) throws UserContactNotFoundException; 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/impl/AttachmentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service.impl; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.stereotype.Service; 5 | import su.foxochat.constant.StorageConstant; 6 | import su.foxochat.dto.api.request.AttachmentAddDTO; 7 | import su.foxochat.dto.api.response.UploadAttachmentDTO; 8 | import su.foxochat.dto.internal.AttachmentPresignedDTO; 9 | import su.foxochat.exception.message.AttachmentsCannotBeEmpty; 10 | import su.foxochat.exception.message.UnknownAttachmentsException; 11 | import su.foxochat.model.Attachment; 12 | import su.foxochat.model.User; 13 | import su.foxochat.repository.AttachmentRepository; 14 | import su.foxochat.service.AttachmentService; 15 | import su.foxochat.service.StorageService; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | @Slf4j 21 | @Service 22 | public class AttachmentServiceImpl implements AttachmentService { 23 | 24 | public final AttachmentRepository attachmentRepository; 25 | 26 | public final StorageService storageService; 27 | 28 | public AttachmentServiceImpl(AttachmentRepository attachmentRepository, StorageService storageService) { 29 | this.attachmentRepository = attachmentRepository; 30 | this.storageService = storageService; 31 | } 32 | 33 | @Override 34 | public AttachmentPresignedDTO getPresignedURLAndSave(AttachmentAddDTO attachment, User user) { 35 | AttachmentPresignedDTO dto = storageService.getPresignedUrl(StorageConstant.ATTACHMENTS_BUCKET); 36 | Attachment attachmentObj = attachmentRepository.save(new Attachment(user, dto.getUuid(), attachment.getFilename(), attachment.getContentType(), 0, true)); 37 | 38 | log.debug("Successfully got presigned url and saved attachment {}", dto.getUuid()); 39 | return new AttachmentPresignedDTO(dto.getUrl(), dto.getUuid(), attachmentObj); 40 | } 41 | 42 | @Override 43 | public List uploadAll(User user, List attachments) { 44 | List attachmentsData = new ArrayList<>(); 45 | 46 | attachments.forEach(attachment -> { 47 | AttachmentPresignedDTO dto = getPresignedURLAndSave(attachment, user); 48 | attachmentsData.add(new UploadAttachmentDTO(dto.getUrl(), dto.getAttachment().getId())); 49 | }); 50 | 51 | log.debug("Successfully uploaded all attachments by user {}", user.getUsername()); 52 | return attachmentsData; 53 | } 54 | 55 | @Override 56 | public AttachmentPresignedDTO upload(User user, AttachmentAddDTO attachment) throws UnknownAttachmentsException, AttachmentsCannotBeEmpty { 57 | if (attachment == null) throw new AttachmentsCannotBeEmpty(); 58 | 59 | AttachmentPresignedDTO dto = getPresignedURLAndSave(attachment, user); 60 | 61 | if (user != null && dto.getAttachment().getUser().getId() != user.getId()) { 62 | throw new UnknownAttachmentsException(); 63 | } 64 | 65 | log.debug("Successfully uploaded attachment by user {}", user.getUsername()); 66 | return dto; 67 | } 68 | 69 | @Override 70 | public List get(User user, List attachmentsIds) throws UnknownAttachmentsException { 71 | List attachments = new ArrayList<>(); 72 | 73 | if (!attachmentsIds.isEmpty()) { 74 | for (Long id : attachmentsIds) { 75 | Attachment attachment = attachmentRepository.findById(id).orElseThrow(UnknownAttachmentsException::new); 76 | 77 | if (attachment.getUser().getId() != user.getId()) throw new UnknownAttachmentsException(); 78 | 79 | attachments.add(attachment); 80 | } 81 | } 82 | 83 | log.debug("Successfully got all attachments by user {}", user.getUsername()); 84 | return attachments; 85 | } 86 | 87 | @Override 88 | public Attachment getById(long id) throws UnknownAttachmentsException { 89 | return attachmentRepository.findById(id).orElseThrow(UnknownAttachmentsException::new); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/impl/AuthenticationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service.impl; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import io.jsonwebtoken.Jwts; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Service; 8 | import su.foxochat.config.APIConfig; 9 | import su.foxochat.constant.EmailConstant; 10 | import su.foxochat.constant.OTPConstant; 11 | import su.foxochat.constant.UserConstant; 12 | import su.foxochat.dto.api.request.UserResetPasswordConfirmDTO; 13 | import su.foxochat.dto.api.request.UserResetPasswordDTO; 14 | import su.foxochat.exception.otp.NeedToWaitBeforeResendException; 15 | import su.foxochat.exception.otp.OTPExpiredException; 16 | import su.foxochat.exception.otp.OTPsInvalidException; 17 | import su.foxochat.exception.user.UserCredentialsDuplicateException; 18 | import su.foxochat.exception.user.UserCredentialsIsInvalidException; 19 | import su.foxochat.exception.user.UserEmailNotVerifiedException; 20 | import su.foxochat.exception.user.UserUnauthorizedException; 21 | import su.foxochat.model.OTP; 22 | import su.foxochat.model.User; 23 | import su.foxochat.service.*; 24 | import su.foxochat.util.OTPGenerator; 25 | import su.foxochat.util.PasswordHasher; 26 | 27 | import java.nio.charset.StandardCharsets; 28 | import java.util.Base64; 29 | import java.util.Map; 30 | 31 | @Slf4j 32 | @Service 33 | public class AuthenticationServiceImpl implements AuthenticationService { 34 | 35 | private final UserService userService; 36 | 37 | private final EmailService emailService; 38 | 39 | private final JwtService jwtService; 40 | 41 | private final OTPService otpService; 42 | 43 | private final APIConfig apiConfig; 44 | 45 | private final ObjectMapper objectMapper; 46 | 47 | public AuthenticationServiceImpl(UserService userService, EmailService emailService, JwtService jwtService, OTPService otpService, APIConfig apiConfig, ObjectMapper objectMapper) { 48 | this.userService = userService; 49 | this.emailService = emailService; 50 | this.jwtService = jwtService; 51 | this.otpService = otpService; 52 | this.apiConfig = apiConfig; 53 | this.objectMapper = objectMapper; 54 | } 55 | 56 | public User getUser(String token, boolean ignoreEmailVerification, boolean removeBearerFromString) throws UserUnauthorizedException, UserEmailNotVerifiedException { 57 | if (token.startsWith("Bearer ")) { 58 | token = token.substring(7); 59 | } 60 | 61 | User user; 62 | 63 | try { 64 | String[] parts = token.split("\\."); 65 | String payload = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8); 66 | Map claims = objectMapper.readValue(payload, new TypeReference<>() { 67 | }); 68 | long userId = Long.parseLong((String) claims.get("jti")); 69 | 70 | user = userService.getById(userId).orElseThrow(UserUnauthorizedException::new); 71 | 72 | Jwts.parser().verifyWith(jwtService.getSigningKey(user.getTokenVersion())).build().parseSignedClaims(token); 73 | } catch (Exception e) { 74 | throw new UserUnauthorizedException(); 75 | } 76 | 77 | if (!ignoreEmailVerification && user.hasFlag(UserConstant.Flags.EMAIL_VERIFIED)) 78 | throw new UserEmailNotVerifiedException(); 79 | 80 | return user; 81 | } 82 | 83 | public String register(String username, String email, String password) throws UserCredentialsDuplicateException { 84 | User user = userService.add(username, email, password); 85 | 86 | log.debug("User ({}) created successfully", user.getUsername()); 87 | 88 | if (!apiConfig.isDevelopment()) { 89 | sendConfirmationEmail(user); 90 | 91 | log.debug("User ({}) email verification message sent successfully", user.getUsername()); 92 | } 93 | 94 | return jwtService.generate(user); 95 | } 96 | 97 | private void sendConfirmationEmail(User user) { 98 | String emailType = EmailConstant.Type.EMAIL_VERIFY.getValue(); 99 | String digitCode = OTPGenerator.generateDigitCode(); 100 | long issuedAt = System.currentTimeMillis(); 101 | long expiresAt = issuedAt + OTPConstant.Lifetime.BASE.getValue(); 102 | String accessToken = jwtService.generate(user); 103 | 104 | emailService.send(user.getEmail(), user.getId(), emailType, user.getUsername(), digitCode, issuedAt, expiresAt, accessToken); 105 | } 106 | 107 | public String login(String email, String password) throws UserCredentialsIsInvalidException { 108 | User user = userService.getByEmail(email).orElseThrow(UserCredentialsIsInvalidException::new); 109 | if (!PasswordHasher.verifyPassword(password, user.getPassword())) throw new UserCredentialsIsInvalidException(); 110 | 111 | log.debug("User ({}) login successfully", user.getUsername()); 112 | return jwtService.generate(user); 113 | } 114 | 115 | public void verifyEmail(User user, String pathCode) throws OTPsInvalidException, OTPExpiredException { 116 | OTP OTP = otpService.validate(pathCode); 117 | 118 | userService.updateFlags(user, UserConstant.Flags.AWAITING_CONFIRMATION, UserConstant.Flags.EMAIL_VERIFIED); 119 | log.debug("User ({}) email verified successfully", user.getUsername()); 120 | 121 | otpService.delete(OTP); 122 | } 123 | 124 | public void resendEmail(User user, String accessToken) throws OTPsInvalidException, NeedToWaitBeforeResendException { 125 | if (apiConfig.isDevelopment()) return; 126 | 127 | OTP OTP = otpService.getByUserId(user.getId()); 128 | 129 | if (OTP == null) throw new OTPsInvalidException(); 130 | 131 | long issuedAt = OTP.getIssuedAt(); 132 | if (System.currentTimeMillis() - issuedAt < OTPConstant.Lifetime.RESEND.getValue()) 133 | throw new NeedToWaitBeforeResendException(); 134 | 135 | log.debug("User ({}) email resend successfully", user.getUsername()); 136 | emailService.send(user.getEmail(), user.getId(), OTP.getType(), user.getUsername(), OTP.getValue(), System.currentTimeMillis(), OTP.getExpiresAt(), accessToken); 137 | } 138 | 139 | public void resetPassword(UserResetPasswordDTO body) throws UserCredentialsIsInvalidException { 140 | User user = userService.getByEmail(body.getEmail()).orElseThrow(UserCredentialsIsInvalidException::new); 141 | 142 | String type = EmailConstant.Type.EMAIL_VERIFY.getValue(); 143 | String value = OTPGenerator.generateDigitCode(); 144 | long issuedAt = System.currentTimeMillis(); 145 | long expiresAt = issuedAt + OTPConstant.Lifetime.BASE.getValue(); 146 | 147 | user.addFlag(UserConstant.Flags.AWAITING_CONFIRMATION); 148 | 149 | emailService.send(user.getEmail(), user.getId(), type, user.getUsername(), value, System.currentTimeMillis(), expiresAt, null); 150 | log.debug("User ({}) reset password requested successfully", user.getUsername()); 151 | } 152 | 153 | public void confirmResetPassword(UserResetPasswordConfirmDTO body) throws OTPExpiredException, OTPsInvalidException, UserCredentialsIsInvalidException { 154 | User user = userService.getByEmail(body.getEmail()).orElseThrow(UserCredentialsIsInvalidException::new); 155 | OTP OTP = otpService.validate(body.getOTP()); 156 | 157 | user.setPassword(PasswordHasher.hashPassword(body.getNewPassword())); 158 | user.setTokenVersion(user.getTokenVersion() + 1); 159 | user.removeFlag(UserConstant.Flags.AWAITING_CONFIRMATION); 160 | 161 | otpService.delete(OTP); 162 | log.debug("User ({}) password reset successfully", user.getUsername()); 163 | } 164 | 165 | public User authUser(String accessToken, boolean ignoreEmailVerification) throws UserUnauthorizedException, UserEmailNotVerifiedException { 166 | if (accessToken == null) throw new UserUnauthorizedException(); 167 | 168 | if (!accessToken.startsWith("Bearer ")) throw new UserUnauthorizedException(); 169 | 170 | return getUser(accessToken, ignoreEmailVerification, true); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/impl/ChannelServiceImpl.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service.impl; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.context.annotation.Lazy; 5 | import org.springframework.dao.DataIntegrityViolationException; 6 | import org.springframework.stereotype.Service; 7 | import su.foxochat.constant.ChannelConstant; 8 | import su.foxochat.constant.GatewayConstant; 9 | import su.foxochat.constant.MemberConstant; 10 | import su.foxochat.dto.api.request.ChannelCreateDTO; 11 | import su.foxochat.dto.api.request.ChannelEditDTO; 12 | import su.foxochat.dto.api.response.ChannelDTO; 13 | import su.foxochat.dto.api.response.MemberDTO; 14 | import su.foxochat.exception.cdn.UploadFailedException; 15 | import su.foxochat.exception.channel.ChannelAlreadyExistException; 16 | import su.foxochat.exception.channel.ChannelNotFoundException; 17 | import su.foxochat.exception.member.MemberAlreadyInChannelException; 18 | import su.foxochat.exception.member.MemberInChannelNotFoundException; 19 | import su.foxochat.exception.member.MissingPermissionsException; 20 | import su.foxochat.exception.message.UnknownAttachmentsException; 21 | import su.foxochat.model.Channel; 22 | import su.foxochat.model.Member; 23 | import su.foxochat.model.User; 24 | import su.foxochat.repository.ChannelRepository; 25 | import su.foxochat.service.AttachmentService; 26 | import su.foxochat.service.GatewayService; 27 | import su.foxochat.service.MemberService; 28 | 29 | import java.util.List; 30 | import java.util.Map; 31 | import java.util.Optional; 32 | import java.util.stream.Collectors; 33 | 34 | @Slf4j 35 | @Service 36 | public class ChannelServiceImpl implements su.foxochat.service.ChannelService { 37 | 38 | private final ChannelRepository channelRepository; 39 | 40 | private final MemberService memberService; 41 | 42 | private final GatewayService gatewayService; 43 | 44 | private final AttachmentService attachmentService; 45 | 46 | public ChannelServiceImpl(ChannelRepository channelRepository, MemberService memberService, @Lazy GatewayService gatewayService, AttachmentService attachmentService) { 47 | this.channelRepository = channelRepository; 48 | this.memberService = memberService; 49 | this.gatewayService = gatewayService; 50 | this.attachmentService = attachmentService; 51 | } 52 | 53 | @Override 54 | public Channel add(User user, ChannelCreateDTO body) throws ChannelAlreadyExistException { 55 | Channel channel; 56 | 57 | long isPublic = 0; 58 | 59 | if (body.isPublic() && body.getType() != ChannelConstant.Type.DM.getType()) 60 | isPublic = ChannelConstant.Flags.PUBLIC.getBit(); 61 | 62 | try { 63 | channel = new Channel(body.getDisplayName(), body.getName(), isPublic, body.getType(), user); 64 | channelRepository.save(channel); 65 | } catch (DataIntegrityViolationException e) { 66 | throw new ChannelAlreadyExistException(); 67 | } 68 | 69 | Member member = new Member(user, channel, MemberConstant.Permissions.ADMIN.getBit()); 70 | memberService.add(member); 71 | 72 | log.debug("Channel ({}) by user ({}) created successfully", channel.getName(), user.getUsername()); 73 | return channel; 74 | } 75 | 76 | @Override 77 | public Channel getById(long id) throws ChannelNotFoundException { 78 | return channelRepository.findById(id).orElseThrow(ChannelNotFoundException::new); 79 | } 80 | 81 | @Override 82 | public Channel getByName(String name) throws ChannelNotFoundException { 83 | Channel channel = channelRepository.findByName(name).orElseThrow(ChannelNotFoundException::new); 84 | if (channel.hasFlag(ChannelConstant.Flags.PUBLIC)) return channel; 85 | throw new ChannelNotFoundException(); 86 | } 87 | 88 | @Override 89 | public Channel update(Member member, Channel channel, ChannelEditDTO body) throws Exception { 90 | if (!member.hasAnyPermission(MemberConstant.Permissions.ADMIN, MemberConstant.Permissions.MANAGE_MESSAGES)) 91 | throw new MissingPermissionsException(); 92 | 93 | try { 94 | if (body.getDisplayName() != null) channel.setDisplayName(body.getDisplayName()); 95 | if (body.getName() != null) channel.setName(body.getName()); 96 | if (body.getIcon() <= 0) { 97 | channel.setIcon(attachmentService.getById(body.getIcon())); 98 | } 99 | 100 | channelRepository.save(channel); 101 | } catch (DataIntegrityViolationException e) { 102 | throw new ChannelAlreadyExistException(); 103 | } catch (UnknownAttachmentsException e) { 104 | throw new UploadFailedException(); 105 | } 106 | 107 | gatewayService.sendMessageToSpecificSessions(getRecipients(channel), GatewayConstant.Opcode.DISPATCH.ordinal(), new ChannelDTO(channel, null), GatewayConstant.Event.CHANNEL_UPDATE.getValue()); 108 | log.debug("Channel ({}) edited successfully", channel.getName()); 109 | return channel; 110 | } 111 | 112 | @Override 113 | public void delete(Channel channel, User user) throws Exception { 114 | Member member = memberService.getByChannelIdAndUserId(channel.getId(), user.getId()) 115 | .orElseThrow(MemberInChannelNotFoundException::new); 116 | 117 | if (!member.hasAnyPermission(MemberConstant.Permissions.ADMIN)) throw new MissingPermissionsException(); 118 | 119 | channelRepository.delete(channel); 120 | gatewayService.sendMessageToSpecificSessions(getRecipients(channel), GatewayConstant.Opcode.DISPATCH.ordinal(), Map.of("id", channel.getId()), GatewayConstant.Event.CHANNEL_DELETE.getValue()); 121 | log.debug("Channel ({}) deleted successfully", channel.getName()); 122 | } 123 | 124 | @Override 125 | public Member addMember(Channel channel, User user) throws Exception { 126 | // check if member not exist in channel 127 | if (memberService.getByChannelIdAndUserId(channel.getId(), user.getId()).isPresent()) 128 | throw new MemberAlreadyInChannelException(); 129 | 130 | Member member = new Member(user, channel, 0); 131 | member.setPermissions(MemberConstant.Permissions.ATTACH_FILES, MemberConstant.Permissions.SEND_MESSAGES); 132 | log.debug("Member ({}) joined channel ({}) successfully", member.getUser().getUsername(), channel.getName()); 133 | gatewayService.sendMessageToSpecificSessions(getRecipients(channel), GatewayConstant.Opcode.DISPATCH.ordinal(), new MemberDTO(member, true), GatewayConstant.Event.MEMBER_ADD.getValue()); 134 | return memberService.add(member); 135 | } 136 | 137 | @Override 138 | public void removeMember(Channel channel, User user) throws Exception { 139 | Member member = memberService.getByChannelIdAndUserId(channel.getId(), user.getId()).orElseThrow(MemberInChannelNotFoundException::new); 140 | 141 | memberService.delete(member); 142 | gatewayService.sendMessageToSpecificSessions(getRecipients(channel), GatewayConstant.Opcode.DISPATCH.ordinal(), new MemberDTO(member, true), GatewayConstant.Event.MEMBER_REMOVE.getValue()); 143 | log.debug("Member ({}) left channel ({}) successfully", member.getUser().getUsername(), channel.getName()); 144 | } 145 | 146 | private List getRecipients(Channel channel) { 147 | Optional optChannel = channelRepository.findById(channel.getId()); 148 | 149 | if (optChannel.isPresent()) channel = optChannel.get(); 150 | 151 | return channel.getMembers().stream() 152 | .map(Member::getUser) 153 | .map(User::getId) 154 | .collect(Collectors.toList()); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/impl/EmailServiceImpl.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service.impl; 2 | 3 | import jakarta.mail.MessagingException; 4 | import jakarta.mail.internet.MimeMessage; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.core.io.Resource; 7 | import org.springframework.core.io.ResourceLoader; 8 | import org.springframework.mail.javamail.JavaMailSender; 9 | import org.springframework.mail.javamail.MimeMessageHelper; 10 | import org.springframework.scheduling.annotation.Async; 11 | import org.springframework.stereotype.Service; 12 | import su.foxochat.config.APIConfig; 13 | import su.foxochat.config.EmailConfig; 14 | import su.foxochat.constant.EmailConstant; 15 | import su.foxochat.service.OTPService; 16 | import su.foxochat.util.StringUtils; 17 | 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.nio.charset.StandardCharsets; 21 | 22 | @Slf4j 23 | @Service 24 | public class EmailServiceImpl implements su.foxochat.service.EmailService { 25 | 26 | private final OTPService otpService; 27 | 28 | private final ResourceLoader resourceLoader; 29 | 30 | private final JavaMailSender javaMailSender; 31 | 32 | private final EmailConfig emailConfig; 33 | 34 | private final APIConfig apiConfig; 35 | 36 | public EmailServiceImpl(OTPService otpService, JavaMailSender javaMailSender, ResourceLoader resourceLoader, EmailConfig emailConfig, APIConfig apiConfig) { 37 | this.otpService = otpService; 38 | this.javaMailSender = javaMailSender; 39 | this.resourceLoader = resourceLoader; 40 | this.emailConfig = emailConfig; 41 | this.apiConfig = apiConfig; 42 | } 43 | 44 | @Async 45 | @Override 46 | public void send(String to, long id, String type, String username, String digitCode, long issuedAt, long expiresAt, String token) { 47 | if (apiConfig.isDevelopment()) return; 48 | 49 | MimeMessage mimeMessage = javaMailSender.createMimeMessage(); 50 | MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, StandardCharsets.UTF_8.name()); 51 | 52 | try { 53 | helper.setTo(to); 54 | helper.setFrom(emailConfig.getEmail()); 55 | 56 | String subject = getSubjectByType(type); 57 | String htmlContent = getContentByType(username, digitCode, token); 58 | 59 | helper.setSubject(subject); 60 | helper.setText(htmlContent, true); 61 | 62 | javaMailSender.send(mimeMessage); 63 | log.debug("Email {} sent to {} successfully", type, to); 64 | 65 | otpService.save(id, type, digitCode, issuedAt, expiresAt); 66 | } catch (IllegalArgumentException | MessagingException | IOException e) { 67 | log.error("Error occurred while sending email to {}: {}", to, e.getMessage(), e); 68 | } 69 | } 70 | 71 | private String getSubjectByType(String type) { 72 | type = type.toUpperCase(); 73 | 74 | return switch (EmailConstant.Type.valueOf(type)) { 75 | case ACCOUNT_DELETE -> "Confirm Your Account Deletion"; 76 | case EMAIL_VERIFY -> "Confirm Your Email Address"; 77 | case RESET_PASSWORD -> "Confirm Password Change"; 78 | }; 79 | } 80 | 81 | private String getContentByType(String username, String digitCode, String token) throws IOException { 82 | return readHTML().replace("{0}", username).replace("{1}", digitCode);//.replace("{2}", token); 83 | } 84 | 85 | private String readHTML() throws IOException { 86 | String templateName = "email"; 87 | Resource resource = resourceLoader.getResource("classpath:templates/" + templateName + ".html"); 88 | 89 | if (!resource.exists()) { 90 | log.error("Template not found: {}", templateName); 91 | throw new IOException("Template file not found: " + templateName); 92 | } 93 | 94 | try (InputStream inputStream = resource.getInputStream()) { 95 | return StringUtils.inputStreamToString(inputStream, StandardCharsets.UTF_8); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/impl/GatewayServiceImpl.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service.impl; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.web.socket.TextMessage; 7 | import org.springframework.web.socket.WebSocketSession; 8 | import su.foxochat.dto.gateway.EventDTO; 9 | import su.foxochat.handler.structure.EventHandler; 10 | import su.foxochat.model.Session; 11 | import su.foxochat.service.GatewayService; 12 | 13 | import java.util.List; 14 | import java.util.concurrent.ConcurrentHashMap; 15 | 16 | @Slf4j 17 | @Service 18 | public class GatewayServiceImpl implements GatewayService { 19 | 20 | private final EventHandler webSocketHandler; 21 | 22 | private final ObjectMapper objectMapper; 23 | 24 | public GatewayServiceImpl(EventHandler webSocketHandler, ObjectMapper objectMapper) { 25 | this.webSocketHandler = webSocketHandler; 26 | this.objectMapper = objectMapper; 27 | } 28 | 29 | @Override 30 | public void sendMessageToSpecificSessions(List userIds, int opcode, Object data, String type) throws Exception { 31 | ConcurrentHashMap sessions = webSocketHandler.getSessions(); 32 | for (Session session : sessions.values()) { 33 | if (session != null) { 34 | if (!userIds.contains(session.getUserId())) return; 35 | 36 | int seqNumber = session.getSequence(); 37 | session.increaseSequence(); 38 | WebSocketSession wsSession = session.getWebSocketSession(); 39 | 40 | if (!wsSession.isOpen()) return; 41 | 42 | wsSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(new EventDTO(opcode, data, seqNumber, type)))); 43 | log.debug("Sent message to userIds ({}) with (opcode: {}, type: {})", userIds, opcode, type); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/impl/JwtServiceImpl.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service.impl; 2 | 3 | import io.jsonwebtoken.Jwts; 4 | import io.jsonwebtoken.io.Decoders; 5 | import io.jsonwebtoken.security.Keys; 6 | import org.springframework.stereotype.Service; 7 | import su.foxochat.config.JwtConfig; 8 | import su.foxochat.constant.TokenConstant; 9 | import su.foxochat.model.User; 10 | 11 | import javax.crypto.SecretKey; 12 | import java.util.Date; 13 | 14 | @Service 15 | public class JwtServiceImpl implements su.foxochat.service.JwtService { 16 | 17 | private final JwtConfig jwtConfig; 18 | 19 | public JwtServiceImpl(JwtConfig jwtConfig) { 20 | this.jwtConfig = jwtConfig; 21 | } 22 | 23 | @Override 24 | public String generate(User user) { 25 | long now = System.currentTimeMillis(); 26 | Date expirationDate = new Date(now + TokenConstant.LIFETIME); 27 | 28 | return Jwts.builder() 29 | .id(String.valueOf(user.getId())) 30 | .expiration(expirationDate) 31 | .signWith(getSigningKey(user.getTokenVersion())) 32 | .compact(); 33 | } 34 | 35 | @Override 36 | public SecretKey getSigningKey(int tokenVersion) { 37 | return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtConfig.getSecret() + tokenVersion)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/impl/MemberServiceImpl.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service.impl; 2 | 3 | import org.springframework.stereotype.Service; 4 | import su.foxochat.model.Channel; 5 | import su.foxochat.model.Member; 6 | import su.foxochat.repository.MemberRepository; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | import java.util.stream.Collectors; 11 | 12 | @Service 13 | public class MemberServiceImpl implements su.foxochat.service.MemberService { 14 | 15 | private final MemberRepository memberRepository; 16 | 17 | public MemberServiceImpl(MemberRepository memberRepository) { 18 | this.memberRepository = memberRepository; 19 | } 20 | 21 | @Override 22 | public List getChannelsByUserId(long userId) { 23 | return memberRepository.findAllByUserId(userId) 24 | .stream() 25 | .map(Member::getChannel) 26 | .collect(Collectors.toList()); 27 | } 28 | 29 | @Override 30 | public List getAllByChannelId(long channelId) { 31 | return memberRepository.findAllByChannelId(channelId); 32 | } 33 | 34 | @Override 35 | public Optional getByChannelIdAndUserId(long channelId, long userId) { 36 | return memberRepository.findByChannelIdAndUserId(channelId, userId); 37 | } 38 | 39 | @Override 40 | public Member add(Member member) { 41 | return memberRepository.save(member); 42 | } 43 | 44 | @Override 45 | public void delete(Member member) { 46 | memberRepository.delete(member); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/impl/MessageServiceImpl.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service.impl; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.stereotype.Service; 5 | import su.foxochat.constant.GatewayConstant; 6 | import su.foxochat.constant.MemberConstant; 7 | import su.foxochat.dto.api.request.AttachmentAddDTO; 8 | import su.foxochat.dto.api.request.MessageCreateDTO; 9 | import su.foxochat.dto.api.response.MessageDTO; 10 | import su.foxochat.dto.api.response.UploadAttachmentDTO; 11 | import su.foxochat.exception.channel.ChannelNotFoundException; 12 | import su.foxochat.exception.member.MemberInChannelNotFoundException; 13 | import su.foxochat.exception.member.MissingPermissionsException; 14 | import su.foxochat.exception.message.AttachmentsCannotBeEmpty; 15 | import su.foxochat.exception.message.MessageNotFoundException; 16 | import su.foxochat.model.*; 17 | import su.foxochat.repository.MessageRepository; 18 | import su.foxochat.service.AttachmentService; 19 | import su.foxochat.service.ChannelService; 20 | import su.foxochat.service.GatewayService; 21 | import su.foxochat.service.MemberService; 22 | 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.stream.Collectors; 27 | 28 | @Slf4j 29 | @Service 30 | public class MessageServiceImpl implements su.foxochat.service.MessageService { 31 | 32 | private final MessageRepository messageRepository; 33 | 34 | private final GatewayService gatewayService; 35 | 36 | private final MemberService memberService; 37 | 38 | private final AttachmentService attachmentService; 39 | 40 | private final ChannelService channelService; 41 | 42 | public MessageServiceImpl(MessageRepository messageRepository, GatewayService gatewayService, MemberService memberService, AttachmentService attachmentService, ChannelService channelService) { 43 | this.messageRepository = messageRepository; 44 | this.gatewayService = gatewayService; 45 | this.memberService = memberService; 46 | this.attachmentService = attachmentService; 47 | this.channelService = channelService; 48 | } 49 | 50 | @Override 51 | public List getAllByChannel(long before, int limit, Channel channel) { 52 | List messagesArray = messageRepository.findAllByChannel(channel, before, limit); 53 | 54 | log.debug("Messages ({}, {}) in channel ({}) found successfully", limit, before, channel.getId()); 55 | 56 | return messagesArray.reversed(); 57 | } 58 | 59 | @Override 60 | public Message getByIdAndChannel(long id, Channel channel) throws MessageNotFoundException { 61 | Message message = messageRepository.findByChannelAndId(channel, id).orElseThrow(MessageNotFoundException::new); 62 | 63 | log.debug("Message {} in channel {} found successfully", id, channel.getId()); 64 | 65 | return message; 66 | } 67 | 68 | @Override 69 | public Message add(Channel channel, User user, MessageCreateDTO body) throws Exception { 70 | Member member = memberService.getByChannelIdAndUserId(channel.getId(), user.getId()).orElseThrow(MemberInChannelNotFoundException::new); 71 | 72 | if (!member.hasAnyPermission(MemberConstant.Permissions.ADMIN, MemberConstant.Permissions.SEND_MESSAGES)) 73 | throw new MissingPermissionsException(); 74 | 75 | List attachments = new ArrayList<>(); 76 | if (body.getAttachments() != null) attachments = attachmentService.get(user, body.getAttachments()); 77 | 78 | Message message = new Message(channel, body.getContent(), member, attachments); 79 | messageRepository.save(message); 80 | 81 | gatewayService.sendMessageToSpecificSessions(getRecipients(channel), GatewayConstant.Opcode.DISPATCH.ordinal(), new MessageDTO(message, true), GatewayConstant.Event.MESSAGE_CREATE.getValue()); 82 | log.debug("Message {} to channel {} created successfully", message.getId(), channel.getId()); 83 | 84 | return message; 85 | } 86 | 87 | @Override 88 | public List addAttachments(Channel channel, User user, List attachments) throws MissingPermissionsException, AttachmentsCannotBeEmpty, MemberInChannelNotFoundException { 89 | if (attachments.isEmpty()) throw new AttachmentsCannotBeEmpty(); 90 | 91 | Member member = memberService.getByChannelIdAndUserId(channel.getId(), user.getId()).orElseThrow(MemberInChannelNotFoundException::new); 92 | 93 | if (!member.hasAnyPermission(MemberConstant.Permissions.ADMIN, MemberConstant.Permissions.SEND_MESSAGES)) 94 | throw new MissingPermissionsException(); 95 | 96 | log.debug("Successfully added attachments to message {} by user {}", channel.getId(), user.getId()); 97 | return attachmentService.uploadAll(user, attachments); 98 | } 99 | 100 | @Override 101 | public void delete(long id, Member member, Channel channel) throws Exception { 102 | Message message = messageRepository.findByChannelAndId(channel, id).orElseThrow(MessageNotFoundException::new); 103 | 104 | if (!message.isAuthor(member) && !member.hasAnyPermission(MemberConstant.Permissions.ADMIN, MemberConstant.Permissions.MANAGE_MESSAGES)) 105 | throw new MissingPermissionsException(); 106 | 107 | messageRepository.delete(message); 108 | gatewayService.sendMessageToSpecificSessions(getRecipients(channel), GatewayConstant.Opcode.DISPATCH.ordinal(), Map.of("id", id), GatewayConstant.Event.MESSAGE_DELETE.getValue()); 109 | log.debug("Message {} in channel {} deleted successfully", id, channel.getId()); 110 | } 111 | 112 | @Override 113 | public Message update(long id, Channel channel, Member member, MessageCreateDTO body) throws Exception { 114 | Message message = messageRepository.findByChannelAndId(channel, id).orElseThrow(MessageNotFoundException::new); 115 | String content = body.getContent(); 116 | 117 | if (!message.isAuthor(member)) throw new MissingPermissionsException(); 118 | 119 | message.setContent(content); 120 | messageRepository.save(message); 121 | 122 | gatewayService.sendMessageToSpecificSessions(getRecipients(channel), GatewayConstant.Opcode.DISPATCH.ordinal(), new MessageDTO(message, true), GatewayConstant.Event.MESSAGE_UPDATE.getValue()); 123 | log.debug("Message {} in channel {} edited successfully", id, channel.getId()); 124 | 125 | return message; 126 | } 127 | 128 | @Override 129 | public Message getLastByChannel(Channel channel) { 130 | return messageRepository.getLastMessageByChannel(channel).orElse(null); 131 | } 132 | 133 | private List getRecipients(Channel channel) throws ChannelNotFoundException { 134 | return channelService.getById(channel.getId()) 135 | .getMembers().stream() 136 | .map(Member::getUser) 137 | .map(User::getId) 138 | .collect(Collectors.toList()); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/impl/OTPServiceImpl.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service.impl; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.stereotype.Service; 5 | import su.foxochat.exception.otp.OTPExpiredException; 6 | import su.foxochat.exception.otp.OTPsInvalidException; 7 | import su.foxochat.model.OTP; 8 | import su.foxochat.repository.OTPRepository; 9 | 10 | @Slf4j 11 | @Service 12 | public class OTPServiceImpl implements su.foxochat.service.OTPService { 13 | 14 | private final OTPRepository otpRepository; 15 | 16 | public OTPServiceImpl(OTPRepository otpRepository) { 17 | this.otpRepository = otpRepository; 18 | } 19 | 20 | @Override 21 | public OTP validate(String pathCode) throws OTPsInvalidException, OTPExpiredException { 22 | 23 | OTP OTP = otpRepository.findByValue(pathCode).orElseThrow(OTPsInvalidException::new); 24 | 25 | if (OTP.expiresAt <= System.currentTimeMillis()) 26 | throw new OTPExpiredException(); 27 | 28 | log.debug("OTP ({}) for user ({}) validated successfully", OTP.getValue(), OTP.getUserId()); 29 | 30 | return OTP; 31 | } 32 | 33 | @Override 34 | public void delete(OTP OTP) { 35 | otpRepository.delete(OTP); 36 | log.debug("OTP ({}, {}) deleted successfully", OTP.getValue(), OTP.getUserId()); 37 | } 38 | 39 | @Override 40 | public void save(long id, String type, String digitCode, long issuedAt, long expiresAt) { 41 | OTP OTP = new OTP(id, type, digitCode, issuedAt, expiresAt); 42 | otpRepository.save(OTP); 43 | log.debug("OTP ({}, {}) saved successfully", OTP.getValue(), OTP.getUserId()); 44 | } 45 | 46 | @Override 47 | public OTP getByUserId(long userId) throws OTPsInvalidException { 48 | return otpRepository.findByUserId(userId).orElseThrow(OTPsInvalidException::new); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/service/impl/StorageServiceImpl.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.service.impl; 2 | 3 | import io.minio.GetPresignedObjectUrlArgs; 4 | import io.minio.MinioAsyncClient; 5 | import io.minio.http.Method; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.stereotype.Service; 8 | import su.foxochat.dto.internal.AttachmentPresignedDTO; 9 | 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.UUID; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | @Slf4j 16 | @Service 17 | public class StorageServiceImpl implements su.foxochat.service.StorageService { 18 | 19 | private final MinioAsyncClient minioClient; 20 | 21 | public StorageServiceImpl(MinioAsyncClient minioClient) { 22 | this.minioClient = minioClient; 23 | } 24 | 25 | @Override 26 | public AttachmentPresignedDTO getPresignedUrl(String bucketName) { 27 | Map reqParams = new HashMap<>(); 28 | reqParams.put("response-content-type", "application/json"); 29 | 30 | String url; 31 | String uuid = String.valueOf(UUID.randomUUID()); 32 | 33 | try { 34 | url = minioClient.getPresignedObjectUrl( 35 | GetPresignedObjectUrlArgs.builder() 36 | .method(Method.PUT) 37 | .bucket(bucketName) 38 | .object(uuid) 39 | .expiry(10, TimeUnit.MINUTES) 40 | .extraQueryParams(reqParams) 41 | .build()); 42 | } catch (Exception e) { 43 | throw new RuntimeException(e); 44 | } 45 | 46 | log.debug("Successfully get presigned url to bucket {} with uuid {}", bucketName, uuid); 47 | return new AttachmentPresignedDTO(url, uuid, null); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/util/OTPGenerator.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.util; 2 | 3 | import java.util.Random; 4 | 5 | public class OTPGenerator { 6 | 7 | public static String generateDigitCode() { 8 | Random random = new Random(); 9 | int min = 1; 10 | int max = 999999; 11 | 12 | int generatedNumber = random.nextInt((max - min) + 1) + min; 13 | return String.format("%06d", generatedNumber); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/util/PasswordHasher.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.util; 2 | 3 | import org.mindrot.jbcrypt.BCrypt; 4 | 5 | @SuppressWarnings("BooleanMethodIsAlwaysInverted") 6 | public class PasswordHasher { 7 | 8 | public static String hashPassword(String password) { 9 | return BCrypt.hashpw(password, BCrypt.gensalt()); 10 | } 11 | 12 | @SuppressWarnings("BooleanMethodIsAlwaysInverted") 13 | public static boolean verifyPassword(String password, String hashedPassword) { 14 | return BCrypt.checkpw(password, hashedPassword); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/su/foxochat/util/StringUtils.java: -------------------------------------------------------------------------------- 1 | package su.foxochat.util; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.nio.charset.Charset; 7 | 8 | public class StringUtils { 9 | 10 | public static String inputStreamToString(InputStream inputStream, Charset charsetName) throws IOException { 11 | ByteArrayOutputStream result = new ByteArrayOutputStream(); 12 | byte[] buffer = new byte[1024]; 13 | 14 | for (int length; (length = inputStream.read(buffer)) != -1; ) { 15 | result.write(buffer, 0, length); 16 | } 17 | 18 | return result.toString(charsetName); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/application.example.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | threads: 3 | virtual: 4 | enabled: true 5 | jmx: 6 | enabled: false 7 | datasource: 8 | driver-class-name: org.postgresql.Driver 9 | username: foxochat 10 | password: passwd 11 | url: jdbc:postgresql://localhost:5432/foxochat 12 | jpa: 13 | hibernate: 14 | ddl-auto: none 15 | open-in-view: true 16 | data: 17 | jpa: 18 | repositories: 19 | bootstrap-mode: deferred 20 | jackson: 21 | property-naming-strategy: SNAKE_CASE 22 | 23 | logging: 24 | level: 25 | root: INFO 26 | org.springframework: INFO 27 | 28 | springdoc: 29 | swagger-ui: 30 | enabled: false 31 | 32 | smtp: 33 | host: smtp.mailersend.net 34 | port: 587 35 | username: user 36 | password: pass 37 | email: noreply@foxochat.su 38 | 39 | minio: 40 | url: https://min.io # minio api url 41 | name: # access key 42 | secret: # secret key 43 | 44 | jwt: 45 | secret: # random secret key 46 | 47 | api: 48 | version: 1 49 | env: dev # dev or prod 50 | url: http://localhost:8080 51 | cdn: 52 | url: https://cdn.foxochat.su 53 | gateway: 54 | production_url: wss://api.foxochat.su 55 | development_url: wss://api-dev.foxochat.su 56 | app: 57 | production_url: https://api.foxochat.su 58 | development_url: https://api-dev.foxochat.su 59 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V10__.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | DROP COLUMN contact_id; 3 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V11__.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX idx_user_contact; 2 | CREATE UNIQUE INDEX idx_user_contact ON user_contacts (user_id, contact_id); 3 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V12__.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | DROP COLUMN key; 3 | 4 | ALTER TABLE users 5 | RENAME COLUMN deletion TO deleted_at; 6 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V13__.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | ADD COLUMN token_version INTEGER NOT NULL default 0; 3 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V1__.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE attachments 2 | ( 3 | id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, 4 | user_id BIGINT NOT NULL, 5 | uuid VARCHAR(255), 6 | filename VARCHAR(255), 7 | content_type VARCHAR(255), 8 | flags BIGINT, 9 | CONSTRAINT pk_attachments PRIMARY KEY (id) 10 | ); 11 | 12 | CREATE TABLE channels 13 | ( 14 | id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, 15 | display_name VARCHAR(255), 16 | name VARCHAR(255), 17 | icon BIGINT NOT NULL, 18 | type INTEGER, 19 | flags BIGINT, 20 | user_id BIGINT NOT NULL, 21 | created_at BIGINT, 22 | CONSTRAINT pk_channels PRIMARY KEY (id) 23 | ); 24 | 25 | CREATE TABLE members 26 | ( 27 | id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, 28 | permissions BIGINT, 29 | user_id BIGINT NOT NULL, 30 | channel BIGINT NOT NULL, 31 | joined_at BIGINT, 32 | CONSTRAINT pk_members PRIMARY KEY (id) 33 | ); 34 | 35 | CREATE TABLE messages 36 | ( 37 | id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, 38 | content TEXT, 39 | author BIGINT NOT NULL, 40 | timestamp BIGINT, 41 | channel BIGINT NOT NULL, 42 | CONSTRAINT pk_messages PRIMARY KEY (id) 43 | ); 44 | 45 | CREATE TABLE otps 46 | ( 47 | user_id BIGINT NOT NULL, 48 | type VARCHAR(255), 49 | value VARCHAR(255), 50 | issued_at BIGINT, 51 | expires_at BIGINT, 52 | CONSTRAINT pk_otps PRIMARY KEY (user_id) 53 | ); 54 | 55 | CREATE TABLE users 56 | ( 57 | id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, 58 | display_name VARCHAR(255), 59 | username VARCHAR(255), 60 | email VARCHAR(255), 61 | password VARCHAR(255), 62 | avatar BIGINT NOT NULL, 63 | flags BIGINT, 64 | type INTEGER, 65 | created_at BIGINT, 66 | deletion BIGINT, 67 | key VARCHAR(255), 68 | CONSTRAINT pk_users PRIMARY KEY (id) 69 | ); 70 | 71 | CREATE INDEX idx_attachment_user_id ON attachments (id, user_id); 72 | 73 | CREATE UNIQUE INDEX idx_channel_name ON channels (name); 74 | 75 | CREATE INDEX idx_member_user_channel ON members (user_id, channel); 76 | 77 | CREATE INDEX idx_message_id_channel_id ON messages (id, channel); 78 | 79 | CREATE UNIQUE INDEX idx_otp_value ON otps (value); 80 | 81 | CREATE UNIQUE INDEX idx_user_email ON users (email); 82 | 83 | CREATE UNIQUE INDEX idx_user_username ON users (username); 84 | 85 | ALTER TABLE attachments 86 | ADD CONSTRAINT FK_ATTACHMENTS_ON_USER FOREIGN KEY (user_id) REFERENCES users (id); 87 | 88 | ALTER TABLE channels 89 | ADD CONSTRAINT FK_CHANNELS_ON_ICON FOREIGN KEY (icon) REFERENCES attachments (id); 90 | 91 | ALTER TABLE channels 92 | ADD CONSTRAINT FK_CHANNELS_ON_USER FOREIGN KEY (user_id) REFERENCES users (id); 93 | 94 | ALTER TABLE members 95 | ADD CONSTRAINT FK_MEMBERS_ON_CHANNEL FOREIGN KEY (channel) REFERENCES channels (id); 96 | 97 | ALTER TABLE members 98 | ADD CONSTRAINT FK_MEMBERS_ON_USER FOREIGN KEY (user_id) REFERENCES users (id); 99 | 100 | ALTER TABLE messages 101 | ADD CONSTRAINT FK_MESSAGES_ON_AUTHOR FOREIGN KEY (author) REFERENCES members (id); 102 | 103 | ALTER TABLE messages 104 | ADD CONSTRAINT FK_MESSAGES_ON_CHANNEL FOREIGN KEY (channel) REFERENCES channels (id); 105 | 106 | ALTER TABLE users 107 | ADD CONSTRAINT FK_USERS_ON_AVATAR FOREIGN KEY (avatar) REFERENCES attachments (id); -------------------------------------------------------------------------------- /src/main/resources/db/migration/V2__.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | ALTER COLUMN avatar DROP NOT NULL; 3 | 4 | ALTER TABLE channels 5 | ALTER COLUMN icon DROP NOT NULL; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V3__.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE members 2 | RENAME COLUMN channel TO channel_id; 3 | 4 | ALTER TABLE messages 5 | RENAME COLUMN channel TO channel_id; -------------------------------------------------------------------------------- /src/main/resources/db/migration/V4__.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | RENAME COLUMN avatar TO avatar_id; 3 | 4 | ALTER TABLE channels 5 | RENAME COLUMN icon TO icon_id; 6 | 7 | ALTER TABLE attachments 8 | ADD COLUMN message_id BIGINT; 9 | 10 | CREATE TABLE message_attachments 11 | ( 12 | id SERIAL PRIMARY KEY, 13 | message_id BIGINT NOT NULL, 14 | attachment_id BIGINT NOT NULL, 15 | CONSTRAINT fk_message FOREIGN KEY (message_id) REFERENCES messages (id) ON DELETE CASCADE, 16 | CONSTRAINT fk_attachment FOREIGN KEY (attachment_id) REFERENCES attachments (id) ON DELETE CASCADE 17 | ); 18 | 19 | CREATE INDEX idx_message_attachment ON message_attachments (message_id, attachment_id); 20 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V5__.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE message_attachments 2 | ADD CONSTRAINT uq_attachment_id UNIQUE (attachment_id); 3 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V6__.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | ADD COLUMN status INTEGER, 3 | ADD COLUMN status_update_timestamp BIGINT; 4 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V7__.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | RENAME COLUMN status_update_timestamp TO status_updated_at; 3 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V8__.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE attachments 2 | DROP COLUMN message_id; 3 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V9__.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE users 2 | ADD contact_id BIGINT; 3 | 4 | CREATE TABLE user_contacts 5 | ( 6 | id SERIAL PRIMARY KEY, 7 | user_id BIGINT NOT NULL, 8 | contact_id BIGINT NOT NULL, 9 | CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, 10 | CONSTRAINT fk_contact FOREIGN KEY (contact_id) REFERENCES users (id) ON DELETE CASCADE 11 | ); 12 | 13 | CREATE INDEX idx_user_contact ON user_contacts (user_id, contact_id); 14 | -------------------------------------------------------------------------------- /src/main/resources/templates/email.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Email Code 8 | 9 | 10 | 11 | 12 | 13 | 42 | 43 |
14 | 15 | 16 | 19 | 20 | 21 | 31 | 32 | 33 | 39 | 40 | 41 |
44 | 45 | 46 | --------------------------------------------------------------------------------