From 7b43916a4de43f03426222317cbed8fd02404001 Mon Sep 17 00:00:00 2001 From: Rodrigo Prestes Machado Date: Sat, 22 Nov 2025 09:36:00 -0300 Subject: [PATCH 1/4] LoginResponseDTO as a standard response object from all endpoints --- CHANGELOG.md | 12 +++- pom.xml | 2 +- .../adapters/controllers/UserController.kt | 70 ++++++++++++------- .../rest/authentication/AuthenticationWS.kt | 19 ++--- .../rest/authentication/SocialAuthWS.kt | 16 ++--- .../rest/authentication/TwoFactorAuth.kt | 8 +-- .../rest/authentication/WebAuthnWS.kt | 8 +-- 7 files changed, 75 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 176de57..78bb284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,20 @@ # Users change Log +## 0.0.6 + +- LoginResponseDTO as a standard response object from all endpoints +- + ## 0.0.5 - Port to Kotlin +- webauthn (experimental) ## 0.0.4 - - Clean Architecture - - Updated to Quarkus 3 - - Updated to Java 20 Temurin +- Clean Architecture +- Updated to Quarkus 3 +- Updated to Java 20 Temurin ## 0.0.3 diff --git a/pom.xml b/pom.xml index 636d176..12eea31 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 dev.orion users - 0.0.5 + 0.0.6 3.12.1 21 diff --git a/src/main/kotlin/dev/orion/users/adapters/controllers/UserController.kt b/src/main/kotlin/dev/orion/users/adapters/controllers/UserController.kt index e5c1429..8ab0a4b 100644 --- a/src/main/kotlin/dev/orion/users/adapters/controllers/UserController.kt +++ b/src/main/kotlin/dev/orion/users/adapters/controllers/UserController.kt @@ -175,20 +175,25 @@ class UserController : BasicController() { /** * Creates a user, generates a Json Web Token and returns a - * AuthenticationDTO object. + * LoginResponseDTO object. * * @param name : The user name * @param email : The user e-mail * @param password : The user password - * @return A Uni object + * @return A Uni object */ - fun createAuthenticate(name: String, email: String, password: String): Uni { + fun createAuthenticate(name: String, email: String, password: String): Uni { return this.createUser(name, email, password) .onItem().ifNotNull().transform { user -> - val dto = AuthenticationDTO() - dto.token = this.generateJWT(user) - dto.user = user - dto + val authDto = AuthenticationDTO() + authDto.token = this.generateJWT(user) + authDto.user = user + + val response = LoginResponseDTO() + response.authentication = authDto + response.requires2FA = false + + response } } @@ -298,9 +303,9 @@ class UserController : BasicController() { * * @param email The email of the user * @param code The TOTP code to validate - * @return A Uni that emits an AuthenticationDTO with JWT if validation succeeds + * @return A Uni that emits a LoginResponseDTO with JWT if validation succeeds */ - fun validateSocialLogin2FA(email: String, code: String): Uni { + fun validateSocialLogin2FA(email: String, code: String): Uni { // Validate code format using use case val user: User = twoFactorAuthUC.validateCode(email, code) @@ -332,10 +337,15 @@ class UserController : BasicController() { } // Generate JWT and return DTO - val dto = AuthenticationDTO() - dto.token = generateJWT(userEntity) - dto.user = userEntity - Uni.createFrom().item(dto) + val authDto = AuthenticationDTO() + authDto.token = generateJWT(userEntity) + authDto.user = userEntity + + val response = LoginResponseDTO() + response.authentication = authDto + response.requires2FA = false + + Uni.createFrom().item(response) } } @@ -344,9 +354,9 @@ class UserController : BasicController() { * * @param email The email of the user * @param code The TOTP code to validate - * @return A Uni that emits an AuthenticationDTO with JWT if validation succeeds + * @return A Uni that emits a LoginResponseDTO with JWT if validation succeeds */ - fun validate2FACode(email: String, code: String): Uni { + fun validate2FACode(email: String, code: String): Uni { // Validate code format using use case val user: User = twoFactorAuthUC.validateCode(email, code) @@ -373,10 +383,15 @@ class UserController : BasicController() { } // Generate JWT and return DTO - val dto = AuthenticationDTO() - dto.token = generateJWT(userEntity) - dto.user = userEntity - Uni.createFrom().item(dto) + val authDto = AuthenticationDTO() + authDto.token = generateJWT(userEntity) + authDto.user = userEntity + + val response = LoginResponseDTO() + response.authentication = authDto + response.requires2FA = false + + Uni.createFrom().item(response) } } @@ -540,9 +555,9 @@ class UserController : BasicController() { * * @param email The email of the user * @param response The authentication response from the client (JSON string) - * @return An AuthenticationDTO with JWT if authentication succeeds + * @return A LoginResponseDTO with JWT if authentication succeeds */ - fun finishWebAuthnAuthentication(email: String, response: String): Uni { + fun finishWebAuthnAuthentication(email: String, response: String): Uni { // Validate using use case webAuthnUC.finishAuthentication(email, response) @@ -566,10 +581,15 @@ class UserController : BasicController() { webAuthnCredentialRepository.saveCredential(credential) // Generate JWT and return DTO - val dto = AuthenticationDTO() - dto.token = generateJWT(user) - dto.user = user - dto + val authDto = AuthenticationDTO() + authDto.token = generateJWT(user) + authDto.user = user + + val loginResponse = LoginResponseDTO() + loginResponse.authentication = authDto + loginResponse.requires2FA = false + + loginResponse } } } diff --git a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/AuthenticationWS.kt b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/AuthenticationWS.kt index 4b77a0b..257f94c 100644 --- a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/AuthenticationWS.kt +++ b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/AuthenticationWS.kt @@ -103,13 +103,8 @@ class AuthenticationWS { return controller.login(email, password) .onItem().ifNotNull() .transform { response -> - if (response.requires2FA) { - // Return 200 OK but indicate 2FA is required - Response.ok(response).status(Response.Status.OK).build() - } else { - // Normal login response - Response.ok(response.authentication).build() - } + // Always return LoginResponseDTO complete + Response.ok(response).build() } .onItem().ifNull() .failWith(ServiceException("User not found", Response.Status.UNAUTHORIZED)) @@ -124,7 +119,7 @@ class AuthenticationWS { * * @param email The email of the user * @param code The TOTP code - * @return The AuthenticationDTO with JWT token + * @return The LoginResponseDTO with JWT token * @throws A ServiceException if validation fails */ @POST @@ -138,8 +133,8 @@ class AuthenticationWS { @RestForm @NotEmpty code: String ): Uni { return controller.validate2FACode(email, code) - .onItem().transform { dto -> - Response.ok(dto).build() + .onItem().transform { response -> + Response.ok(response).build() } .onFailure().transform { e -> val message = e.message ?: "Invalid TOTP code" @@ -153,7 +148,7 @@ class AuthenticationWS { * @param name The name of the user * @param email The email of the user * @param password The password of the user - * @return The Authentication DTO + * @return The LoginResponseDTO * @throws A Bad Request if the service is unable to create the user */ @POST @@ -168,7 +163,7 @@ class AuthenticationWS { @FormParam("password") @NotEmpty password: String ): Uni { return controller.createAuthenticate(name, email, password) - .onItem().ifNotNull().transform { dto -> Response.ok(dto).build() } + .onItem().ifNotNull().transform { response -> Response.ok(response).build() } .onFailure().transform { e -> val message = e.message ?: "Unknown error" throw ServiceException(message, Response.Status.BAD_REQUEST) diff --git a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/SocialAuthWS.kt b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/SocialAuthWS.kt index 00ac293..52b8356 100644 --- a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/SocialAuthWS.kt +++ b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/SocialAuthWS.kt @@ -17,7 +17,6 @@ package dev.orion.users.frameworks.rest.authentication import dev.orion.users.adapters.controllers.UserController -import dev.orion.users.adapters.presenters.AuthenticationDTO import dev.orion.users.adapters.presenters.LoginResponseDTO import dev.orion.users.frameworks.rest.ServiceException import io.quarkus.hibernate.reactive.panache.common.WithSession @@ -90,13 +89,8 @@ class SocialAuthWS { } .onItem().transformToUni { responseUni -> responseUni.onItem().transform { response -> - if (response.requires2FA) { - // Return 200 OK but indicate 2FA is required - Response.ok(response).status(Response.Status.OK).build() - } else { - // Normal login response - Response.ok(response.authentication).build() - } + // Always return LoginResponseDTO complete + Response.ok(response).build() } } .onFailure().transform { e -> @@ -110,7 +104,7 @@ class SocialAuthWS { * * @param email The email of the user * @param code The TOTP code to validate - * @return AuthenticationDTO with JWT token + * @return LoginResponseDTO with JWT token * @throws ServiceException if validation fails */ @POST @@ -124,8 +118,8 @@ class SocialAuthWS { @RestForm @NotEmpty code: String ): Uni { return controller.validateSocialLogin2FA(email, code) - .onItem().transform { dto -> - Response.ok(dto).build() + .onItem().transform { response -> + Response.ok(response).build() } .onFailure().transform { e -> val message = e.message ?: "Invalid TOTP code" diff --git a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/TwoFactorAuth.kt b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/TwoFactorAuth.kt index e65bfe7..d5c2148 100644 --- a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/TwoFactorAuth.kt +++ b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/TwoFactorAuth.kt @@ -17,7 +17,7 @@ package dev.orion.users.frameworks.rest.authentication import dev.orion.users.adapters.controllers.UserController -import dev.orion.users.adapters.presenters.AuthenticationDTO +import dev.orion.users.adapters.presenters.LoginResponseDTO import dev.orion.users.frameworks.rest.ServiceException import io.quarkus.hibernate.reactive.panache.common.WithSession import io.smallrye.mutiny.Uni @@ -85,7 +85,7 @@ class TwoFactorAuth { * * @param email The email of the user * @param code The TOTP code to validate - * @return The AuthenticationDTO with JWT token + * @return The LoginResponseDTO with JWT token * @throws ServiceException if validation fails */ @POST @@ -99,8 +99,8 @@ class TwoFactorAuth { @RestForm @NotEmpty code: String ): Uni { return controller.validate2FACode(email, code) - .onItem().transform { dto -> - Response.ok(dto).build() + .onItem().transform { response -> + Response.ok(response).build() } .onFailure().transform { e -> val message = e.message ?: "Invalid TOTP code" diff --git a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/WebAuthnWS.kt b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/WebAuthnWS.kt index a1f631b..d2d52ca 100644 --- a/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/WebAuthnWS.kt +++ b/src/main/kotlin/dev/orion/users/frameworks/rest/authentication/WebAuthnWS.kt @@ -17,7 +17,7 @@ package dev.orion.users.frameworks.rest.authentication import dev.orion.users.adapters.controllers.UserController -import dev.orion.users.adapters.presenters.AuthenticationDTO +import dev.orion.users.adapters.presenters.LoginResponseDTO import dev.orion.users.frameworks.rest.ServiceException import io.quarkus.hibernate.reactive.panache.common.WithSession import io.smallrye.mutiny.Uni @@ -142,7 +142,7 @@ class WebAuthnWS { * * @param email The email of the user * @param response The authentication response from the client (JSON string) - * @return The AuthenticationDTO with JWT token + * @return The LoginResponseDTO with JWT token * @throws ServiceException if authentication fails */ @POST @@ -156,8 +156,8 @@ class WebAuthnWS { @RestForm @NotEmpty response: String ): Uni { return controller.finishWebAuthnAuthentication(email, response) - .onItem().transform { dto -> - Response.ok(dto).build() + .onItem().transform { loginResponse -> + Response.ok(loginResponse).build() } .onFailure().transform { e -> val message = e.message ?: "Invalid WebAuthn authentication" From 680a96af93994efd659c04a9bb8c0c59e8d6805e Mon Sep 17 00:00:00 2001 From: Rodrigo Prestes Machado Date: Sat, 22 Nov 2025 09:45:59 -0300 Subject: [PATCH 2/4] documentation update --- docs/usecases/Autenticate/Authenticate.md | 93 --------------- docs/usecases/Autenticate/login.md | 137 ++++++++++++++++++++++ docs/usecases/UseCases.puml | 2 +- 3 files changed, 138 insertions(+), 94 deletions(-) delete mode 100644 docs/usecases/Autenticate/Authenticate.md create mode 100644 docs/usecases/Autenticate/login.md diff --git a/docs/usecases/Autenticate/Authenticate.md b/docs/usecases/Autenticate/Authenticate.md deleted file mode 100644 index ba6ad36..0000000 --- a/docs/usecases/Autenticate/Authenticate.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -layout: default -title: Authenticate -parent: Use Cases -nav_order: 1 ---- - -## Authenticate - -This use case is responsible for authenticate a user in the system. - -### Normal flow - -* A client sends a e-mail and password. -* The service validates the input data and verifies if the users exists in the - system. If the users exists, the service returns a JSON with the user data - and a signed JWT. - -## HTTPS endpoints - -* /users/login - * Method: POST - * Consumes: application/x-www-form-urlencoded - * Produces: application/json - -* Request: - -```shell -curl -X POST \ - 'http://localhost:8080/users/login' \ - --header 'Accept: */*' \ - --header 'User-Agent: Thunder Client (https://www.thunderclient.com)' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'email=orion@services.dev' \ - --data-urlencode 'password=12345678' -``` - -* Response: - -```json -{ -"user": { - "hash": "53012a1a-b8ec-40f4-a81e-bc8b97ddab75", - "name": "Orion", - "email": "orion@services.dev", - "emailValid": false, - "secret2FA": null, - "using2FA": false -}, -"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJvcmlvbi11c2VycyIsInVwbiI6Im9yaW9uQHNlcnZpY2VzLmRldiIsImdyb3VwcyI6WyJ1c2VyIl0sImNfaGFzaCI6IjUzMDEyYTFhLWI4ZWMtNDBmNC1hODFlLWJjOGI5N2RkYWI3NSIsImVtYWlsIjoib3Jpb25Ac2VydmljZXMuZGV2IiwiaWF0IjoxNzE1Mzk0NzA0LCJleHAiOjE3MTUzOTUwMDQsImp0aSI6ImMzYjZkZmFkLTAyMDAtNDc3YS05MDJmLTU0ZDg5YjdiMTUzYyJ9.I93SpcxIm31wfMQeiFLuUuuWuwlG-C0aGascSEDseRueILn9Tf5shEyNDMLQr6QRNhQbNjRjnCwe_quenVfjBEF_BLgtDDq7maoqpzDdrnDoKxtxex0dIXmRg2ABZoktB-jBo8yJcflandp1FUe7hG1VduE2E8D6WqvUQiNrhhCiiEZ4d5Moc1H11S3YGg3X1U-QnWUGx70FYQG4Qo-1Ini7T6miC0xCxSJRxumXKKtBRLYMDizp5qPIVoVIatJUu4WgoVZWliStmE7wBu6X_La7z4rAddgIlGRiqLZPkaSruzO2PP3i_T1Ezupcw9ol6LP_nlPaOQHeAjJ7aSQMyA" -} -``` - -## Social Authentication - -The system also supports authentication via social providers (Google). - -### Google Login - -* Endpoint: `/users/login/google` -* Method: POST -* Consumes: application/x-www-form-urlencoded -* Produces: application/json - -* Request: - -```shell -curl -X POST \ - 'http://localhost:8080/users/login/google' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'idToken=GOOGLE_ID_TOKEN' -``` - -* Response: Same as normal login - AuthenticationDTO with user and token. - -### Social Authentication Flow - -1. User clicks "Login with Google" in the frontend -2. Frontend initiates OAuth2 flow with the provider -3. Provider returns an ID token (JWT) -4. Frontend sends the ID token to the backend endpoint -5. Backend validates the token and extracts user information (email, name) -6. Backend searches for user by email -7. If user doesn't exist, backend creates it automatically -8. Backend generates a JWT token for the system -9. Backend returns AuthenticationDTO with user and token - -## Exceptions - -RESTful Web Service layer will return a HTTP 401 (Unauthorized) if the user -does not exist or the password is incorrect. If the request is invalid, for -example, without the required parameters, the service will return a HTTP 400 -(Bad Request). \ No newline at end of file diff --git a/docs/usecases/Autenticate/login.md b/docs/usecases/Autenticate/login.md new file mode 100644 index 0000000..2bc9fda --- /dev/null +++ b/docs/usecases/Autenticate/login.md @@ -0,0 +1,137 @@ +--- +layout: default +title: Login +parent: Use Cases +nav_order: 1 +--- + +## Login + +This use case is responsible for authenticating a user in the system. The endpoint returns a `LoginResponseDTO` that may contain authentication credentials or indicate that two-factor authentication (2FA) is required. + +### Normal flow + +* A client sends an e-mail and password. +* The service validates the input data and verifies if the user exists in the system. +* If the user exists and has 2FA enabled with `require2FAForBasicLogin` set to `true`, the service returns a `LoginResponseDTO` indicating that 2FA is required. +* If the user exists and 2FA is not required, the service returns a `LoginResponseDTO` containing the user data and a signed JWT token. + +### LoginResponseDTO + +The `/users/login` endpoint returns a `LoginResponseDTO` object that can represent different authentication states: + +**Structure:** + +```json +{ + "authentication": { + "user": { + "hash": "string", + "name": "string", + "email": "string", + "emailValid": boolean, + "secret2FA": "string | null", + "using2FA": boolean + }, + "token": "string (JWT)" + }, + "requires2FA": boolean, + "message": "string | null" +} +``` + +**Fields:** + +- `authentication` (AuthenticationDTO | null): Contains the user data and JWT token when login is successful. This field is `null` when 2FA is required. +- `requires2FA` (boolean): Indicates whether two-factor authentication is required to complete the login process. +- `message` (string | null): Optional message providing additional information, typically "2FA code required" when 2FA is needed. + +**AuthenticationDTO Structure:** + +When `authentication` is present, it contains: + +- `user` (UserEntity): The authenticated user object containing: + - `hash`: Unique identifier for the user + - `name`: User's display name + - `email`: User's email address + - `emailValid`: Whether the email has been validated + - `secret2FA`: The 2FA secret (null if not configured) + - `using2FA`: Whether 2FA is enabled for this user +- `token` (string): A signed JWT token that can be used for authenticated requests + +## HTTPS endpoints + +* /users/login + * Method: POST + * Consumes: application/x-www-form-urlencoded + * Produces: application/json + +### Request Example + +```shell +curl -X POST \ + 'http://localhost:8080/users/login' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'email=orion@services.dev' \ + --data-urlencode 'password=12345678' +``` + +### Response Examples + +#### Example 1: Successful Login (without 2FA requirement) + +When the user exists and 2FA is not required, the response contains the authentication data: + +```json +{ + "authentication": { + "user": { + "hash": "53012a1a-b8ec-40f4-a81e-bc8b97ddab75", + "name": "Orion", + "email": "orion@services.dev", + "emailValid": false, + "secret2FA": null, + "using2FA": false + }, + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJvcmlvbi11c2VycyIsInVwbiI6Im9yaW9uQHNlcnZpY2VzLmRldiIsImdyb3VwcyI6WyJ1c2VyIl0sImNfaGFzaCI6IjUzMDEyYTFhLWI4ZWMtNDBmNC1hODFlLWJjOGI5N2RkYWI3NSIsImVtYWlsIjoib3Jpb25Ac2VydmljZXMuZGV2IiwiaWF0IjoxNzE1Mzk0NzA0LCJleHAiOjE3MTUzOTUwMDQsImp0aSI6ImMzYjZkZmFkLTAyMDAtNDc3YS05MDJmLTU0ZDg5YjdiMTUzYyJ9.I93SpcxIm31wfMQeiFLuUuuWuwlG-C0aGascSEDseRueILn9Tf5shEyNDMLQr6QRNhQbNjRjnCwe_quenVfjBEF_BLgtDDq7maoqpzDdrnDoKxtxex0dIXmRg2ABZoktB-jBo8yJcflandp1FUe7hG1VduE2E8D6WqvUQiNrhhCiiEZ4d5Moc1H11S3YGg3X1U-QnWUGx70FYQG4Qo-1Ini7T6miC0xCxSJRxumXKKtBRLYMDizp5qPIVoVIatJUu4WgoVZWliStmE7wBu6X_La7z4rAddgIlGRiqLZPkaSruzO2PP3i_T1Ezupcw9ol6LP_nlPaOQHeAjJ7aSQMyA" + }, + "requires2FA": false, + "message": null +} +``` + +#### Example 2: Login Requiring 2FA + +When the user has 2FA enabled and `require2FAForBasicLogin` is set to `true`, the response indicates that a 2FA code is required: + +```json +{ + "authentication": null, + "requires2FA": true, + "message": "2FA code required" +} +``` + +In this case, the client should: +1. Extract the 2FA code from the user (typically from an authenticator app) +2. Make a subsequent request to `/users/login/2fa` with the email and 2FA code +3. The 2FA endpoint will return a complete `LoginResponseDTO` with authentication data upon successful validation + +### Handling the Response + +**When `requires2FA` is `false`:** +- Use the `token` from `authentication.token` for subsequent authenticated requests +- Store the token securely (e.g., in localStorage or httpOnly cookie) +- Include the token in the `Authorization` header as `Bearer {token}` + +**When `requires2FA` is `true`:** +- Prompt the user for their 2FA code +- Make a POST request to `/users/login/2fa` with: + - `email`: The user's email + - `code`: The 2FA code from the authenticator app +- The 2FA endpoint will return a `LoginResponseDTO` with `authentication` populated upon success + +## Exceptions + +RESTful Web Service layer will return a HTTP 401 (Unauthorized) if the user does not exist or the password is incorrect. If the request is invalid, for example, without the required parameters, the service will return a HTTP 400 (Bad Request). + diff --git a/docs/usecases/UseCases.puml b/docs/usecases/UseCases.puml index afb3a42..8da7557 100644 --- a/docs/usecases/UseCases.puml +++ b/docs/usecases/UseCases.puml @@ -4,7 +4,7 @@ left to right direction actor "Client" as client rectangle Users{ - usecase "Authenticate" as UC1 + usecase "Login" as UC1 usecase "Create and Authenticate" as UC2 usecase "Create User" as UC3 usecase "Validate E-mail" as UC4 From 5d126caad47a0af9b3927e419181c846574b80e1 Mon Sep 17 00:00:00 2001 From: Rodrigo Prestes Machado Date: Sat, 22 Nov 2025 14:38:50 -0300 Subject: [PATCH 3/4] new documentation --- CHANGELOG.md | 2 +- README.md | 16 +- .../Frontend.md => playground/Playground.md} | 517 +++++------------ docs/running/Running.md | 467 ++++++++++++++++ docs/usecases/CreateUser/create.md | 67 ++- docs/usecases/DeleteUser/delete.md | 75 +-- docs/usecases/SocialAuth/SocialAuth.md | 524 ++++++++---------- .../TwoFactorAuth/sequenceGenerateQrCode.puml | 40 +- .../TwoFactorAuth/sequenceValidateCode.puml | 32 +- docs/usecases/TwoFactorAuth/twofactorauth.md | 252 +++++++-- docs/usecases/UpdateUser/updateUser.md | 206 ++++--- docs/usecases/ValidateEmail/validateEmail.md | 51 +- docs/usuario/2FA.md | 153 ----- docs/usuario/WebAuthn.md | 279 ---------- docs/usuario/index.md | 72 --- 15 files changed, 1353 insertions(+), 1400 deletions(-) rename docs/{frontend/Frontend.md => playground/Playground.md} (53%) create mode 100644 docs/running/Running.md delete mode 100644 docs/usuario/2FA.md delete mode 100644 docs/usuario/WebAuthn.md delete mode 100644 docs/usuario/index.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 78bb284..418458d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## 0.0.6 - LoginResponseDTO as a standard response object from all endpoints -- +- New documentation ## 0.0.5 diff --git a/README.md b/README.md index bdba76e..7b03c30 100755 --- a/README.md +++ b/README.md @@ -31,9 +31,11 @@ You can run your application in dev mode that enables live coding using: > **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. -## Frontend Playground +For comprehensive instructions on running the service in development, production with containers (JVM and native compilation), configuration, and troubleshooting, see the [Running the Service Documentation](docs/running/Running.md). -The project includes a Vue 3 frontend playground application that provides a user interface for testing all features of the Orion Users service. +## Playground + +The project includes a Vue 3 playground application that provides a user interface for testing and experimenting with all features of the Orion Users service. **Access the playground**: After starting the application, navigate to `http://localhost:8080/test` @@ -45,7 +47,7 @@ The playground includes: - Password recovery - User profile management -For detailed information about the frontend playground, including development setup and configuration, see the [Frontend Documentation](docs/frontend/Frontend.md). +For detailed information about the playground, including how to run it in development and production modes, social login configuration, and user guide, see the [Playground Documentation](docs/playground/Playground.md). ## Packaging and running the application @@ -54,7 +56,7 @@ The application can be packaged using: ./mvnw package ``` It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory. -Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory. +Be aware that it's not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory. The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`. @@ -77,7 +79,9 @@ Or, if you don't have GraalVM installed, you can run the native executable build ./mvnw package -Pnative -Dquarkus.native.container-build=true ``` -You can then execute your native executable with: `./target/users-0.0.1-runner` +You can then execute your native executable with: `./target/users-0.0.6-runner` + +For detailed instructions on building and running containers (JVM and native), see the [Running the Service Documentation](docs/running/Running.md). If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling. @@ -92,6 +96,8 @@ The service provides the following main endpoints: - `GET /users/validateEmail` - Validate user email with code - `POST /users/google/2FAuth/qrCode` - Generate 2FA QR code - `POST /users/google/2FAuth/validate` - Validate 2FA code +- `POST /users/login/2fa` - Login with 2FA code +- `POST /users/login/google` - Social authentication with Google For complete API documentation, see the [documentation site](https://users.orion-services.dev). diff --git a/docs/frontend/Frontend.md b/docs/playground/Playground.md similarity index 53% rename from docs/frontend/Frontend.md rename to docs/playground/Playground.md index c1fa0c5..972811a 100644 --- a/docs/frontend/Frontend.md +++ b/docs/playground/Playground.md @@ -1,35 +1,14 @@ --- layout: default -title: Frontend Documentation +title: Playground Documentation nav_order: 3 --- -# Frontend Documentation +# Playground Documentation -This document provides a comprehensive guide to using and developing the Orion Users frontend application. +The Playground is a Vue 3 application built with Vuetify that provides a user interface for testing and experimenting with all features of the Orion Users service. It is located in the `playground` directory and is served by the Quarkus backend at the `/test` URL path. -## Overview - -The Orion Users frontend is a Vue 3 application built with Vuetify that provides a user interface for all user management and authentication features of the Orion Users service. The frontend application is located in the `playground` directory and is served by the Quarkus backend at the `/test` URL path. - -## Quick Start - -### Running the Playground - -The playground application is integrated with the Quarkus backend and is accessible at: - -**`http://localhost:8080/test`** - -To run the playground: - -1. **Start the Quarkus backend** (this will serve the compiled frontend): -```bash -./mvnw compile quarkus:dev -``` - -2. **Access the application**: - - Open your browser and navigate to: `http://localhost:8080/test` - - The application will be available at this URL +## Running the Playground ### Development Mode @@ -51,128 +30,175 @@ npm run dev - Development server: `http://localhost:3000/test` - The Vite dev server proxies API requests to the Quarkus backend -**Note**: After making changes, rebuild the application (`npm run build`) for the changes to be available when accessing via the Quarkus backend at `/test`. - -## Features - -- ✅ User registration -- ✅ Simple login with email and password -- ✅ Social authentication (Google) -- ✅ Two-factor authentication (2FA) -- ✅ WebAuthn (biometric/security key authentication) -- ✅ Password recovery -- ✅ User profile management -- ✅ Email validation -- ✅ Debug tools for API testing - -## Prerequisites +**Note**: In development mode, Vite runs on port 3000 with hot module replacement. The application is configured to use `/test` as the base path, and API requests to `/users` are automatically proxied to the Quarkus backend running on port 8080. -- **Node.js**: Version 18 or higher -- **npm** or **yarn**: Package manager -- **Backend API**: The Orion Users backend service running (default: `http://localhost:8080`) +### Production Mode -## Location +To run the playground in production mode: -The frontend playground application is located at: -``` -src/main/resources/META-INF/resources/playground/ +1. **Navigate to the playground directory**: +```bash +cd src/main/resources/META-INF/resources/playground ``` -When built, the compiled files are generated in: -``` -src/main/resources/META-INF/resources/test/ +2. **Build the application**: +```bash +npm run build ``` -The application is served by the Quarkus backend at: **`http://localhost:8080/test`** - -## Installation +The built files will be generated in `src/main/resources/META-INF/resources/test/` directory, which is automatically served by Quarkus. -1. Navigate to the playground directory: +3. **Start or restart the Quarkus backend**: ```bash -cd src/main/resources/META-INF/resources/playground +./mvnw compile quarkus:dev ``` -2. Install dependencies: -```bash -npm install +4. **Access the application at**: ``` +http://localhost:8080/test +``` + +**Important**: The application is configured to be served at the `/test` URL path. Make sure to access it at `http://localhost:8080/test` (not at the root `/`). -## Configuration +**Note**: After making changes in development mode, rebuild the application (`npm run build`) for the changes to be available when accessing via the Quarkus backend at `/test`. + +## Social Login Configuration ### Environment Variables Create a `.env` file in the `playground/` directory root: +**Location**: `src/main/resources/META-INF/resources/playground/.env` + ```env # Backend API URL VITE_API_URL=http://localhost:8080 -# Google OAuth2 Client ID (optional, for social login) +# Google OAuth2 Client ID (required for social login) VITE_GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com ``` -### Development Server Configuration +**Important**: +- The `VITE_` prefix is required for Vite to expose the variable to frontend code +- Do not use quotes around the value +- Do not add spaces before or after the `=` +- Replace `your-google-client-id.apps.googleusercontent.com` with the Client ID obtained from Google Cloud Console -The development server is configured in `vite.config.js`: +### Google Client ID Setup -- **Port**: 3000 (default) -- **Proxy**: Automatically proxies `/users` requests to the backend API -- **Hot Module Replacement**: Enabled for fast development +To configure Google OAuth2 for social login: -## Running the Application +1. **Create a project in Google Cloud Console**: + - Access [Google Cloud Console](https://console.cloud.google.com/) + - Create a new project or select an existing one -### Development Mode +2. **Configure OAuth Consent Screen**: + - Navigate to **APIs & Services** > **OAuth consent screen** + - Select **External** for public applications + - Fill in app name, support email, and developer contact + - Add scopes: `openid`, `.../auth/userinfo.email`, `.../auth/userinfo.profile` -1. Start the Quarkus backend server: -```bash -./mvnw compile quarkus:dev -``` +3. **Create OAuth Client ID**: + - Navigate to **APIs & Services** > **Credentials** + - Click **+ Create Credentials** > **OAuth client ID** + - Select **Web application** + - Add authorized JavaScript origins: + - Development: `http://localhost:3000`, `http://localhost:5173` + - Production: `https://your-domain.com` + - Add authorized redirect URIs (same as JavaScript origins) + - Copy the **Client ID** (format: `xxxxx.apps.googleusercontent.com`) -2. In a separate terminal, navigate to the playground directory and start the Vite development server: -```bash -cd src/main/resources/META-INF/resources/playground -npm run dev +4. **Configure in Playground**: + - Add `VITE_GOOGLE_CLIENT_ID` to `.env` file + - Restart the development server + +### Backend Configuration + +Add the Google OAuth2 configuration in the `src/main/resources/application.properties` file: + +```properties +# Google OAuth2 Client ID +social.auth.google.client-id=your-client-id.apps.googleusercontent.com ``` -The Vite development server will be available at `http://localhost:3000/test` (note the `/test` base path). +**Notes**: +- Replace `your-client-id.apps.googleusercontent.com` with the Client ID obtained from Google Cloud Console +- The Client ID must be in the format `xxxxx.apps.googleusercontent.com` +- Do not include spaces or extra characters +- Restart the backend server after modifying the file -**Note**: In development mode, Vite runs on port 3000 with hot module replacement. The application is configured to use `/test` as the base path, and API requests to `/users` are automatically proxied to the Quarkus backend running on port 8080. +**Configuration by Environment**: -### Production Build +For different environments, you can use Quarkus profiles: -1. Navigate to the playground directory: -```bash -cd src/main/resources/META-INF/resources/playground -``` +```properties +# Development +%dev.social.auth.google.client-id=dev-client-id.apps.googleusercontent.com -2. Build the application: -```bash -npm run build +# Test +%test.social.auth.google.client-id=test-client-id.apps.googleusercontent.com + +# Production +%prod.social.auth.google.client-id=prod-client-id.apps.googleusercontent.com ``` -The built files will be generated in `src/main/resources/META-INF/resources/test/` directory, which is automatically served by Quarkus. +### Verify Google Identity Services Script -3. Start or restart the Quarkus backend: -```bash -./mvnw compile quarkus:dev -``` +The Google Identity Services script should be included in the `index.html` file: -4. Access the application at: -``` -http://localhost:8080/test +**Location**: `src/main/resources/META-INF/resources/playground/index.html` + +```html + ``` -**Important**: The application is configured to be served at the `/test` URL path. Make sure to access it at `http://localhost:8080/test` (not at the root `/`). +**Checks**: +- The script should be in the `` of the document +- The `async defer` attributes ensure the script does not block page loading -### Preview Production Build +### Testing Social Login -To preview the production build locally with Vite: +1. Access the login page +2. Verify that the "Login with Google" button is visible +3. Click the "Login with Google" button +4. A Google popup should appear for account selection and authentication +5. After authenticating, you should be redirected to the dashboard -```bash -npm run preview +## System Overview + +### Features + +The Playground provides a comprehensive interface for testing all features of the Orion Users service: + +- ✅ **User Registration** - Create new user accounts +- ✅ **Simple Login** - Email and password authentication +- ✅ **Social Authentication** - Google OAuth2 login +- ✅ **Two-Factor Authentication (2FA)** - TOTP-based 2FA setup and validation +- ✅ **WebAuthn** - Biometric and security key authentication +- ✅ **Password Recovery** - Reset forgotten passwords +- ✅ **User Profile Management** - Update email and password +- ✅ **Email Validation** - Verify email addresses +- ✅ **Debug Tools** - API request/response logging for testing + +### Prerequisites + +- **Node.js**: Version 18 or higher +- **npm** or **yarn**: Package manager +- **Backend API**: The Orion Users backend service running (default: `http://localhost:8080`) + +### Location + +The playground application is located at: +``` +src/main/resources/META-INF/resources/playground/ ``` -This will serve the built files using Vite's preview server. However, for production-like testing, it's recommended to use the Quarkus backend as described above. +When built, the compiled files are generated in: +``` +src/main/resources/META-INF/resources/test/ +``` + +The application is served by the Quarkus backend at: **`http://localhost:8080/test`** ## Project Structure @@ -198,7 +224,7 @@ src/main/resources/META-INF/resources/playground/ │ ├── LoginView.vue # Login and registration page │ ├── DashboardView.vue # User dashboard │ ├── TwoFactorView.vue # 2FA setup and validation -│ ├── WebAuthnView.vue # WebAuthn registration and authentication +│ ├── WebAuthnView.vue # WebAuthn device registration and authentication │ └── RecoverPasswordView.vue # Password recovery ├── index.html # HTML template ├── package.json # Dependencies and scripts @@ -211,7 +237,7 @@ src/main/resources/META-INF/resources/test/ └── ... ``` -## Usage Guide +## User Guide ### User Registration @@ -315,9 +341,25 @@ After logging in, you will be redirected to the dashboard (`/dashboard`) where y - Change your password - Log out -### Viewing API Request/Response Logs +#### Updating Email + +1. Go to the dashboard +2. Enter your new email address in the **Update Email** section +3. Click **Update Email** +4. Check your new email for a validation code +5. Click the validation link in the email + +#### Changing Password + +1. Go to the dashboard +2. Enter your current password +3. Enter your new password (must meet strength requirements) +4. Confirm your new password +5. Click **Change Password** + +### Debug Tools -The frontend includes a built-in debug tool that automatically logs all API requests and responses. This is very useful for debugging and understanding how the API works. +The Playground includes a built-in debug tool that automatically logs all API requests and responses. This is very useful for debugging and understanding how the API works. #### Opening the Debug Modal @@ -372,208 +414,8 @@ Click on any log entry to expand and view detailed information: - Click the **trash icon** (🗑️) in the modal header to clear all logs - Logs are automatically limited to the last 50 entries -#### What Gets Logged - -The debug tool automatically logs: -- All API requests made through the `userApi` service -- Request method, URL, data, and headers -- Response status, data, and headers -- Error information (if request fails) -- Timestamp for each request - -**Example Use Cases:** - -1. **Debugging Login Issues**: - - Check if the request is being sent correctly - - See the exact error message from the API - - Verify the request payload format - -2. **Understanding API Responses**: - - See the exact structure of API responses - - Copy response data for testing - - Understand error formats - -3. **Testing API Endpoints**: - - See what data is being sent - - Verify authentication tokens - - Check request headers - -4. **Development**: - - Monitor all API calls during development - - Debug integration issues - - Understand API behavior - **Note**: The debug logs are stored in browser memory and will be cleared when you refresh the page. They are not persisted to localStorage or sent to any server. -#### Updating Email - -1. Go to the dashboard -2. Enter your new email address in the **Update Email** section -3. Click **Update Email** -4. Check your new email for a validation code -5. Click the validation link in the email - -#### Changing Password - -1. Go to the dashboard -2. Enter your current password -3. Enter your new password (must meet strength requirements) -4. Confirm your new password -5. Click **Change Password** - -## API Service - -The frontend uses a centralized API service located in `src/services/api.js`. All API calls are made through this service. - -### Available Methods - -```javascript -import { userApi } from '@/services/api' - -// Registration -userApi.createUser(name, email, password) -userApi.createAndAuthenticate(name, email, password) - -// Authentication -userApi.login(email, password) -userApi.loginWithGoogle(idToken) - -// Two-Factor Authentication -userApi.generate2FAQRCode(email, password) -userApi.validate2FACode(email, code) -userApi.loginWith2FA(email, code) - -// WebAuthn -userApi.startWebAuthnRegistration(email, origin) -userApi.finishWebAuthnRegistration(email, response, origin, deviceName) -userApi.startWebAuthnAuthentication(email) -userApi.finishWebAuthnAuthentication(email, response) - -// Email Validation -userApi.validateEmail(email, code) - -// Password Recovery -userApi.recoverPassword(email) - -// User Management -userApi.updateUser(email, newEmail, password, newPassword) -``` - -### Request/Response Interceptors - -The API service includes interceptors that: -- Automatically add JWT tokens to authenticated requests -- Handle 401 errors by clearing authentication state -- Log all requests and responses for debugging - -## State Management - -The application uses Pinia for state management. - -### Auth Store - -Located in `src/stores/auth.js`: - -```javascript -import { useAuthStore } from '@/stores/auth' - -const authStore = useAuthStore() - -// Check if user is authenticated -authStore.isAuthenticated - -// Get current user -authStore.user - -// Get auth token -authStore.token - -// Set authentication -authStore.setAuth(token, user) - -// Logout -authStore.logout() -``` - -### Debug Store - -Located in `src/stores/debug.js`: - -```javascript -import { useDebugStore } from '@/stores/debug' - -const debugStore = useDebugStore() - -// View logs -debugStore.logs - -// Clear logs -debugStore.clearLogs() -``` - -## Routing - -The application uses Vue Router for navigation. Routes are defined in `src/router/index.js`. - -### Available Routes - -All routes are prefixed with `/test` when served by Quarkus: - -- `/test/` - Login and registration page -- `/test/dashboard` - User dashboard (requires authentication) -- `/test/2fa` - Two-factor authentication setup and validation -- `/test/webauthn` - WebAuthn device registration and authentication -- `/test/recover-password` - Password recovery - -**Note**: The Vue Router is configured with base path `/test`, so internal navigation will automatically include this prefix. - -### Route Guards - -Routes with `meta: { requiresAuth: true }` are protected and will redirect unauthenticated users to the login page. - -## Components - -### PasswordStrengthIndicator - -Displays password strength in real-time based on validation rules. - -**Props:** -- `password` (String): The password to evaluate - -**Usage:** -```vue - -``` - -### DebugModal - -Modal component for viewing API request/response logs. - -**Usage:** -```vue - -``` - -## Utilities - -### Password Validation - -Located in `src/utils/passwordValidation.js`: - -```javascript -import { getPasswordRules } from '@/utils/passwordValidation' - -const rules = getPasswordRules() -// Returns Vuetify validation rules for password fields -``` - -## Browser Support - -- **Chrome**: 90+ -- **Firefox**: 88+ -- **Safari**: 14+ -- **Edge**: 90+ - ## Troubleshooting ### Application Not Loading at /test @@ -598,6 +440,8 @@ const rules = getPasswordRules() - Restart the development server after adding environment variables - Check browser console for errors - Verify OAuth credentials are correct +- Verify Google Identity Services script is loaded in `index.html` +- Check Google Cloud Console for authorized domains ### WebAuthn Not Working @@ -612,72 +456,6 @@ const rules = getPasswordRules() - Check browser console for errors - Ensure the backend is running and accessible -## Development - -### Adding New Features - -1. Create components in `src/components/` -2. Create views in `src/views/` -3. Add API methods in `src/services/api.js` -4. Add routes in `src/router/index.js` -5. Add state management in `src/stores/` if needed - -### Code Style - -- Use Vue 3 Composition API with ` -``` - -**Verificações**: -- O script deve estar no `` do documento -- Os atributos `async defer` garantem que o script não bloqueie o carregamento da página - -#### 3. Reiniciar o Servidor de Desenvolvimento - -Após adicionar ou modificar variáveis de ambiente: - -1. Pare o servidor de desenvolvimento (se estiver rodando) -2. Reinicie o servidor: - ```bash - cd src/main/resources/META-INF/resources/playground - npm run dev - ``` + - Add the authorized redirect URIs (same values as JavaScript origins) + - For development: `http://localhost:3000`, `http://localhost:5173` + - For production: `https://your-domain.com` -#### 4. Testar a Configuração +5. Click **Create** +6. A popup window will appear with your credentials: + - **Client ID**: Copy this value (format: `xxxxx.apps.googleusercontent.com`) + - **Client secret**: Not required for Google Identity Services (can be ignored) -1. Acesse a página de login -2. Verifique se o botão "Login with Google" está visível -3. Clique no botão "Login with Google" -4. Deve aparecer um popup do Google para seleção de conta e autenticação -5. Após autenticar, você deve ser redirecionado para o dashboard +**Important**: Keep the Client ID secure. You will need it to configure the frontend and backend. -### Configurar o Backend +### Configure Backend -Adicione a configuração do Google OAuth2 no arquivo `src/main/resources/application.properties`: +Add the Google OAuth2 configuration in the `src/main/resources/application.properties` file: ```properties # Google OAuth2 Client ID social.auth.google.client-id=seu-client-id.apps.googleusercontent.com ``` -**Notas**: -- Substitua `seu-client-id.apps.googleusercontent.com` pelo Client ID obtido no Google Cloud Console -- O Client ID deve estar no formato `xxxxx.apps.googleusercontent.com` -- Não inclua espaços ou caracteres extras -- Reinicie o servidor backend após modificar o arquivo +**Notes**: +- Replace `seu-client-id.apps.googleusercontent.com` with the Client ID obtained from Google Cloud Console +- The Client ID must be in the format `xxxxx.apps.googleusercontent.com` +- Do not include spaces or extra characters +- Restart the backend server after modifying the file -**Configuração por Ambiente**: +**Configuration by Environment**: -Para diferentes ambientes, você pode usar perfis do Quarkus: +For different environments, you can use Quarkus profiles: ```properties -# Desenvolvimento +# Development %dev.social.auth.google.client-id=dev-client-id.apps.googleusercontent.com -# Teste +# Test %test.social.auth.google.client-id=test-client-id.apps.googleusercontent.com -# Produção +# Production %prod.social.auth.google.client-id=prod-client-id.apps.googleusercontent.com ``` -## Implementação +### Configure Playground Frontend -### Frontend +#### 1. Create Environment Variables File -O frontend utiliza o Google Identity Services (GIS) para gerenciar o fluxo OAuth2. A implementação está localizada no arquivo `LoginView.vue`. +Create or edit the `.env` file in the playground directory: -#### Fluxo Básico +**Location**: `src/main/resources/META-INF/resources/playground/.env` -1. **Aguarda o Google Identity Services carregar**: - ```javascript - await waitForGoogleIdentityServices() - ``` - -2. **Inicializa o Google Identity Services**: - ```javascript - const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID - - google.accounts.id.initialize({ - client_id: clientId, - callback: async (response) => { - // response.credential contém o ID token - await userApi.loginWithGoogle(response.credential) - } - }) - ``` - -3. **Tenta exibir One Tap**: - ```javascript - google.accounts.id.prompt((notification) => { - if (notification.isNotDisplayed() || notification.isSkippedMoment()) { - // Fallback para OAuth2 Token Client - google.accounts.oauth2.initTokenClient({ - client_id: clientId, - scope: 'openid profile email', - callback: async (tokenResponse) => { - // tokenResponse.access_token contém o access token - await userApi.loginWithGoogle(tokenResponse.access_token) - } - }).requestAccessToken() - } - }) - ``` - -4. **Envia token para o backend**: - ```javascript - // POST /users/login/google - // Content-Type: application/x-www-form-urlencoded - // Body: idToken={token} - const apiResponse = await userApi.loginWithGoogle(token) - ``` - -#### Serviço de API - -O frontend envia o token através do serviço de API (`api.js`): - -```javascript -loginWithGoogle: (idToken) => { - return api.post('/users/login/google', toFormData({ idToken })) -} +```env +VITE_GOOGLE_CLIENT_ID=seu-client-id.apps.googleusercontent.com ``` -### Backend - -O backend valida tokens do Google suportando tanto ID tokens (JWT) quanto access tokens. +**Important**: +- The `VITE_` prefix is required for Vite to expose the variable to frontend code +- Do not use quotes around the value +- Do not add spaces before or after the `=` +- Replace `seu-client-id.apps.googleusercontent.com` with the Client ID obtained from Google Cloud Console -#### Processo de Validação +#### 2. Verify Google Identity Services Script -1. **Normalização**: Remove espaços em branco do token +The Google Identity Services script should be included in the `index.html` file: -2. **Tentativa de Validação como JWT**: - - Verifica se o token tem formato JWT (3 partes separadas por pontos) - - Decodifica o payload (base64url) - - Extrai email e nome do payload +**Location**: `src/main/resources/META-INF/resources/playground/index.html` -3. **Fallback para Access Token**: - - Se não for um JWT válido, assume que é um access token - - Faz chamada à API do Google: `GET https://www.googleapis.com/oauth2/v2/userinfo` - - Extrai email e nome da resposta +```html + +``` -4. **Criação/Busca do Usuário**: - - Busca o usuário pelo email - - Se não existir, cria automaticamente com email validado e role padrão +**Checks**: +- The script should be in the `` of the document +- The `async defer` attributes ensure the script does not block page loading -5. **Geração do JWT do Sistema**: - - Gera um JWT próprio do sistema - - Retorna AuthenticationDTO com usuário e token +#### 3. Restart Development Server -## API Reference +After adding or modifying environment variables: -### Endpoint: Login com Google +1. Stop the development server (if running) +2. Restart the server: + ```bash + cd src/main/resources/META-INF/resources/playground + npm run dev + ``` -**POST** `/users/login/google` +#### 4. Test Configuration -**Content-Type**: `application/x-www-form-urlencoded` +1. Access the login page +2. Verify that the "Login with Google" button is visible +3. Click the "Login with Google" button +4. A Google popup should appear for account selection and authentication +5. After authenticating, you should be redirected to the dashboard -**Parâmetros**: -- `idToken` (String, obrigatório): ID token (JWT) ou access token do Google +## Implementation Details -**Resposta de Sucesso** (200 OK): +### Backend Token Validation Process -```json -{ - "user": { - "hash": "53012a1a-b8ec-40f4-a81e-bc8b97ddab75", - "name": "John Doe", - "email": "john.doe@gmail.com", - "emailValid": true, - "secret2FA": null, - "using2FA": false - }, - "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..." -} -``` +1. **Normalization**: Removes whitespace from the token -**Resposta de Erro** (401 Unauthorized): +2. **JWT Validation Attempt**: + - Verifies if the token has JWT format (3 parts separated by dots) + - Decodes the payload (base64url) + - Extracts email and name from the payload -```json -{ - "message": "Invalid Google token: Token is empty" -} -``` +3. **Fallback to Access Token**: + - If not a valid JWT, assumes it's an access token + - Makes a call to Google API: `GET https://www.googleapis.com/oauth2/v2/userinfo` + - Extracts email and name from the response -**Exemplo com cURL**: +4. **User Creation/Search**: + - Searches for the user by email + - If it doesn't exist, creates automatically with validated email and default role -```bash -curl -X POST \ - 'http://localhost:8080/users/login/google' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'idToken=eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQ1NiJ9...' -``` +5. **System JWT Generation**: + - Generates a system JWT + - Returns AuthenticationDTO with user and token ## Troubleshooting -### Problemas Comuns +### Common Problems -| Problema | Solução | -|----------|---------| -| Botão "Login with Google" não aparece | Verificar script no `index.html` e Client ID no `.env` | -| Popup do Google não abre | Desabilitar bloqueador de popup e verificar domínios autorizados no Google Cloud Console | -| Erro "Invalid Google token" | Fazer login novamente para obter novo token | -| Erro 401 | Verificar Client ID e domínios autorizados | -| Erro CORS | Verificar configuração CORS no backend | -| Email não encontrado | Verificar escopos solicitados (`email`) | -| HTTPS necessário | Configurar HTTPS em produção (exceto localhost) | -| Token expirado | Fazer login novamente | +| Problem | Solution | +|---------|----------| +| "Login with Google" button doesn't appear | Check script in `index.html` and Client ID in `.env` | +| Google popup doesn't open | Disable popup blocker and verify authorized domains in Google Cloud Console | +| "Invalid Google token" error | Log in again to get a new token | +| 401 error | Verify Client ID and authorized domains | +| CORS error | Check CORS configuration in backend | +| Email not found | Verify requested scopes (`email`) | +| HTTPS required | Configure HTTPS in production (except localhost) | +| Token expired | Log in again | -### Verificações Básicas +### Basic Checks -1. **Console do Navegador** (F12): - - Verificar se há erros relacionados ao Google Identity Services - - Verificar se o objeto `google` está disponível: `console.log(typeof google)` +1. **Browser Console** (F12): + - Check for errors related to Google Identity Services + - Verify that the `google` object is available: `console.log(typeof google)` -2. **Variáveis de Ambiente**: - - Verificar se `VITE_GOOGLE_CLIENT_ID` está configurado no `.env` - - Reiniciar servidor após adicionar/modificar variáveis +2. **Environment Variables**: + - Verify that `VITE_GOOGLE_CLIENT_ID` is configured in `.env` + - Restart server after adding/modifying variables 3. **Google Cloud Console**: - - Verificar se os domínios estão em **Authorized JavaScript origins** - - Verificar se os domínios estão em **Authorized redirect URIs** - - Desenvolvimento: `http://localhost:3000`, `http://localhost:5173` - - Produção: `https://your-domain.com` + - Verify that domains are in **Authorized JavaScript origins** + - Verify that domains are in **Authorized redirect URIs** + - Development: `http://localhost:3000`, `http://localhost:5173` + - Production: `https://your-domain.com` 4. **Backend**: - - Verificar se `social.auth.google.client-id` está configurado em `application.properties` - - Verificar logs do backend para erros de validação + - Verify that `social.auth.google.client-id` is configured in `application.properties` + - Check backend logs for validation errors -## Segurança +## Security -### Recomendações para Produção +### Production Recommendations -A implementação atual fornece validação básica adequada para desenvolvimento. Para produção, recomenda-se implementar: +The current implementation provides basic validation adequate for development. For production, it is recommended to implement: -1. **Validação de Assinatura do JWT**: - - Baixar as chaves públicas do Google de `https://www.googleapis.com/oauth2/v3/certs` - - Validar a assinatura do token usando a chave pública correspondente +1. **JWT Signature Validation**: + - Download Google's public keys from `https://www.googleapis.com/oauth2/v3/certs` + - Validate the token signature using the corresponding public key -2. **Validação de Expiração**: - - Verificar o campo `exp` (expiration) do JWT - - Rejeitar tokens expirados +2. **Expiration Validation**: + - Check the `exp` (expiration) field of the JWT + - Reject expired tokens -3. **Validação do Issuer**: - - Verificar que o campo `iss` (issuer) é `https://accounts.google.com` +3. **Issuer Validation**: + - Verify that the `iss` (issuer) field is `https://accounts.google.com` -4. **Validação do Audience**: - - Verificar que o campo `aud` (audience) corresponde ao Client ID configurado +4. **Audience Validation**: + - Verify that the `aud` (audience) field matches the configured Client ID 5. **Rate Limiting**: - - Implementar rate limiting para prevenir abuso do endpoint - -### Boas Práticas + - Implement rate limiting to prevent endpoint abuse -- **Sempre use HTTPS em produção** - O Google requer HTTPS (exceto localhost) -- **Use variáveis de ambiente** - Nunca hardcode credenciais no código -- **Monitore tentativas de autenticação** - Logue tentativas falhadas para detectar abusos -- **Mantenha dependências atualizadas** - Bibliotecas de segurança devem estar atualizadas -- **Valide todos os inputs** - Não confie em dados do cliente sem validação -- **Nunca exponha Client Secrets** - Client secrets nunca devem estar no código do frontend - -## Exceptions +### Best Practices -- **HTTP 401 (Unauthorized)**: Se o token for inválido ou expirado -- **HTTP 400 (Bad Request)**: Se a requisição estiver malformada ou faltando parâmetros obrigatórios +- **Always use HTTPS in production** - Google requires HTTPS (except localhost) +- **Use environment variables** - Never hardcode credentials in code +- **Monitor authentication attempts** - Log failed attempts to detect abuse +- **Keep dependencies updated** - Security libraries should be up to date +- **Validate all inputs** - Don't trust client data without validation +- **Never expose Client Secrets** - Client secrets should never be in frontend code diff --git a/docs/usecases/TwoFactorAuth/sequenceGenerateQrCode.puml b/docs/usecases/TwoFactorAuth/sequenceGenerateQrCode.puml index 7fcf008..7ea610c 100644 --- a/docs/usecases/TwoFactorAuth/sequenceGenerateQrCode.puml +++ b/docs/usecases/TwoFactorAuth/sequenceGenerateQrCode.puml @@ -3,33 +3,37 @@ actor "User agent" as user autonumber - user -> WebService: @POST /api/user/google/2FAuth/qrCode (email,password) + user -> WebService: @POST /users/google/2FAuth/qrCode (email,password) activate WebService - WebService -> UseCase: authenticate(email,password) + WebService -> UseCase: generateQRCode(email,password) activate UseCase - UseCase --> UseCase : autheticate(email,password) - UseCase -->> WebService : Uni + UseCase -> UseCase: validateCode(email,code) + UseCase -->> WebService: User - WebService -> WebService : user.setUsing2FA(true) - WebService -> UseCase: updateUser(user) + WebService -> Repository: authenticate(email,password) + activate Repository + Repository --> Repository: authenticate(email,password) + Repository -->> WebService: Uni + deactivate Repository - UseCase --> UseCase : updateUser(user) - UseCase -->> WebService: Uni + WebService -> WebService: secretKey = generateSecretKey() + WebService -> WebService: user.setUsing2FA(true) + WebService -> WebService: user.setSecret2FA(secretKey) + WebService -> Repository: updateUser(user) + Repository -->> WebService: Uni deactivate UseCase - WebService -> WebService : secret = user.GetSecret2FA() - WebService -> WebService : userEmail = user.GetSecret2FA() - WebService -> GoogleUtils : getGoogleAuthenticatorBarCode(secret,userEmail, issuer) + WebService -> GoogleUtils: getGoogleAuthenticatorBarCode(secretKey,userEmail, issuer) activate GoogleUtils - GoogleUtils --> GoogleUtils : getgetGoogleAuthenticatorBarCode(secret,userEmail, issuer) - GoogleUtils -->> WebService : String barCodeData - WebService --> GoogleUtils : createQrCode(barCodeData) - GoogleUtils --> GoogleUtils : createQrCode(barCodeData) - GoogleUtils -->> WebService : byte[] qrCode + GoogleUtils --> GoogleUtils: getGoogleAuthenticatorBarCode(secretKey,userEmail, issuer) + GoogleUtils -->> WebService: String barCodeData + WebService -> GoogleUtils: createQrCode(barCodeData) + GoogleUtils --> GoogleUtils: createQrCode(barCodeData) + GoogleUtils -->> WebService: byte[] qrCode deactivate GoogleUtils - WebService -->> user : QrCode Image + WebService -->> user: QrCode Image (image/png) deactivate WebService -@enduml \ No newline at end of file +@enduml diff --git a/docs/usecases/TwoFactorAuth/sequenceValidateCode.puml b/docs/usecases/TwoFactorAuth/sequenceValidateCode.puml index 27bca5a..59bf6ae 100644 --- a/docs/usecases/TwoFactorAuth/sequenceValidateCode.puml +++ b/docs/usecases/TwoFactorAuth/sequenceValidateCode.puml @@ -2,30 +2,32 @@ title Two Factor Authenticate Validate Code actor "User agent" as user autonumber - user -> WebService: @POST /api/user/google/2FAuth/Validate (email,code) + + user -> WebService: @POST /users/google/2FAuth/validate (email,code) activate WebService - WebService --> UseCase : findUserByEmail(email) + WebService -> UseCase: validateCode(email,code) activate UseCase - UseCase -> Repository : findUserByEmail(email) + UseCase -> UseCase: validateCode(email,code) + UseCase -->> WebService: User + + WebService -> Repository: findUserByEmail(email) activate Repository Repository --> Repository: findUserByEmail(email) - activate Repository - - Repository -->> UseCase: Uni + Repository -->> WebService: Uni deactivate Repository - UseCase -->> Webservice : Uni - - WebService --> Webservice : secret = user.getSecret2FA() - WebService --> GoogleUtils : getTOTPCode(secret) + WebService -> WebService: Validate user.isUsing2FA() + WebService -> WebService: secret = user.getSecret2FA() + WebService -> GoogleUtils: getTOTPCode(secret) activate GoogleUtils - GoogleUtils --> GoogleUtils : getTOTPCode(secret) - GoogleUtils -->> Webservice : String userCode + GoogleUtils --> GoogleUtils: getTOTPCode(secret) + GoogleUtils -->> WebService: String expectedCode deactivate GoogleUtils - WebService --> Webservice : Validate user.isUsing2FA() - WebService --> Webservice : Validate !userCode.equals(code) - WebService -->> user : User in JSON + + WebService -> WebService: Validate code.equals(expectedCode) + WebService -> WebService: generateJWT(user) + WebService -->> user: LoginResponseDTO (JSON with JWT token) deactivate WebService @enduml diff --git a/docs/usecases/TwoFactorAuth/twofactorauth.md b/docs/usecases/TwoFactorAuth/twofactorauth.md index a4ad5b9..5026666 100644 --- a/docs/usecases/TwoFactorAuth/twofactorauth.md +++ b/docs/usecases/TwoFactorAuth/twofactorauth.md @@ -1,20 +1,17 @@ --- layout: default -title: Two Factor Authenticate +title: Two Factor Authentication parent: Use Cases nav_order: 9 --- -## Two Factor Autheticate +## Two Factor Authentication -## Normal flow generate qrcode +This use case is responsible for two-factor authentication (2FA) using TOTP (Time-based One-Time Password) codes. The system supports generating QR codes for authenticator app setup and validating TOTP codes for authentication. -* A client sends a e-mail and password -* The service validates the input data and verifies if the users exists in the - system -* If the users exists, generate the qrCode to be vinculated do google authenticator +### Sequence Diagrams -### Sequence diagram of generate qrcode +#### Generate QR Code Flow
@@ -22,14 +19,7 @@ nav_order: 9
-## Normal flow validate code - -* A client sends a e-mail and google auth code -* The service validates the input data and verifies if the users exists in the - system -* If the users exists, authenticate the user and return a signed JWT - -### Sequence diagram of validate code +#### Validate Code Flow
@@ -37,52 +27,210 @@ nav_order: 9
-## HTTPS endpoints +### Normal flow - Generate QR Code -* /users/google/2FAuth/qrCode - * Method: POST - * Consumes: application/x-www-form-urlencoded - * Produces: image/png - * Examples: +* A client sends an e-mail and password. +* The service validates the input data and verifies if the user exists in the system. +* The service authenticates the user with the provided credentials. +* If the user exists and credentials are valid, the service: + * Generates a secret key for 2FA + * Sets `using2FA` to `true` and stores the secret key + * Updates the user in the repository + * Generates a QR code containing the secret key in Google Authenticator format +* The service returns the QR code as a PNG image that can be scanned by authenticator apps (Google Authenticator, Microsoft Authenticator, Authy, etc.). - * Request: +### Normal flow - Validate Code - ```shell - curl -X POST \ - 'http://localhost:8080/users/google/2FAuth/qrCode' \ - --header 'Accept: */*' \ - --header 'User-Agent: Thunder Client (https://www.thunderclient.com)' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'email=orion@test.com' \ - --data-urlencode 'password=12345678' - ``` +* A client sends an e-mail and TOTP code (typically 6 digits from an authenticator app). +* The service validates the input data and verifies if the user exists in the system. +* The service checks if 2FA is enabled for the user. +* If the user exists and 2FA is enabled, the service: + * Retrieves the user's 2FA secret key + * Generates the expected TOTP code using the secret key + * Compares the provided code with the expected code +* If the codes match, the service generates a JWT token and returns a `LoginResponseDTO` containing the user data and token. - * Response: +### Normal flow - Login with 2FA - ```md - ![QRCODE](https://t3.gstatic.com/licensed-image?q=tbn:ANd9GcSh-wrQu254qFaRcoYktJ5QmUhmuUedlbeMaQeaozAVD4lh4ICsGdBNubZ8UlMvWjKC) - ``` +When a user has 2FA enabled and `require2FAForBasicLogin` is set to `true`: -* /users/google/2FAuth/validate +* The user attempts to login via `/users/login` with email and password. +* The service validates credentials and checks if 2FA is required. +* If 2FA is required, the service returns a `LoginResponseDTO` with `requires2FA: true` and `authentication: null`. +* The client prompts the user for their 2FA code. +* The client sends the email and 2FA code to `/users/login/2fa`. +* The service validates the code and returns a `LoginResponseDTO` with `authentication` containing the user data and JWT token. + +## HTTPS endpoints + +### Generate QR Code + +* /users/google/2FAuth/qrCode * Method: POST * Consumes: application/x-www-form-urlencoded * Produces: image/png - * Examples: - * Request: +### Validate Code (Setup/Standalone) - ```shell - curl -X POST \ - 'http://localhost:8080/users/google/2FAuth/qrCode' \ - --header 'Accept: */*' \ - --header 'User-Agent: Thunder Client (https://www.thunderclient.com)' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'email=orion@test.com' \ - --data-urlencode 'code=123456' - ``` +* /users/google/2FAuth/validate + * Method: POST + * Consumes: application/x-www-form-urlencoded + * Produces: application/json - * Response: +### Login with 2FA Code - ```txt - eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJ1cG4iOiJyb2RyaWdvQHRlc3RlLmNvbSIsImdyb3VwcyI6WyJ1c2VyIl0sImNfaGFzaCI6Ijc5NjBjMjk1LWQ0NmEtNGI2NC1hNGZiLTE2ZWQxNGYzZTk1NSIsImlhdCI6MTY1NzgzNzY1MCwiZXhwIjoxNjU3ODM3OTUwLCJqdGkiOiIzZjdlOThhMy1hMTAwLTQxOTQtODM0Ny0yMWQwZjRjNDJhYTgifQ.rsHHrOZ5LStCYXREGw0iN7_y7geraKtMYin2OGVchrFF0iX2Stu6m4KGRXVmd3vx_vU3l7RyBN9aFjAO0mm1ScJ-wzP8DQPsuSm1pgw2RBKtTitvi4M7XjsP9bZyuyzP-hWbB6KPhB3oZSzh91nyqqWTQUJrUDsXnuNP3XUX6YAwlXZd5SrxQeNfvcaJ9N2Cj85hw8L5Nm-20P7dt3yj4IZE5QvZ1JYLyNzWZWkYWyr9ffR9v1q83dbxJMaABL8R1sSFZjBTwsQSQOBNSwkCF1U_x2tqj0aZW1w4cqQnpHYAY32AtgmrDHVfdjyQld1g7Qx42C2AoP_ZTWpxZ9vwDg - ``` \ No newline at end of file +* /users/login/2fa + * Method: POST + * Consumes: application/x-www-form-urlencoded + * Produces: application/json + +### Request Examples + +#### Example 1: Generate QR Code + +```shell +curl -X POST \ + 'http://localhost:8080/users/google/2FAuth/qrCode' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'email=orion@test.com' \ + --data-urlencode 'password=12345678' \ + --output qrcode.png +``` + +#### Example 2: Validate Code (Standalone) + +```shell +curl -X POST \ + 'http://localhost:8080/users/google/2FAuth/validate' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'email=orion@test.com' \ + --data-urlencode 'code=123456' +``` + +#### Example 3: Login with 2FA Code + +```shell +curl -X POST \ + 'http://localhost:8080/users/login/2fa' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'email=orion@test.com' \ + --data-urlencode 'code=123456' +``` + +### Response Examples + +#### Example 1: QR Code Response + +When the QR code is successfully generated, the response is a PNG image: + +``` +[Binary PNG image data] +``` + +The QR code can be scanned with any TOTP-compatible authenticator app. + +#### Example 2: Validate Code Response + +When the code is successfully validated, the response contains a `LoginResponseDTO`: + +```json +{ + "authentication": { + "user": { + "hash": "53012a1a-b8ec-40f4-a81e-bc8b97ddab75", + "name": "Orion", + "email": "orion@test.com", + "emailValid": false, + "secret2FA": "JBSWY3DPEHPK3PXP", + "using2FA": true + }, + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJvcmlvbi11c2VycyIsInVwbiI6Im9yaW9uQHRlc3QuY29tIiwiZ3JvdXBzIjpbInVzZXIiXSwiY19oYXNoIjoiNTMwMTJhMWEtYjhlYy00MGY0LWE4MWUtYmM4Yjk3ZGRhYjc1IiwiZW1haWwiOiJvcmlvblRlc3QuY29tIiwiaWF0IjoxNzE1Mzk0NzA0LCJleHAiOjE3MTUzOTUwMDQsImp0aSI6ImMzYjZkZmFkLTAyMDAtNDc3YS05MDJmLTU0ZDg5YjdiMTUzYyJ9..." + }, + "requires2FA": false, + "message": null +} +``` + +**Response Fields:** + +- `authentication` (AuthenticationDTO): Contains the user data and JWT token +- `user` (UserEntity): The authenticated user object + - `secret2FA`: The 2FA secret key (stored for future validations) + - `using2FA`: Always `true` when 2FA is enabled +- `token` (string): A signed JWT token for authenticated requests +- `requires2FA` (boolean): Always `false` after successful 2FA validation +- `message` (string | null): Typically `null` for successful validation + +## Exceptions + +RESTful Web Service layer will return: +- HTTP 401 (Unauthorized) if: + - The user does not exist + - The password is incorrect (for QR code generation) + - The TOTP code is invalid or incorrect + - 2FA is not enabled for the user + - The 2FA secret is not found +- HTTP 400 (Bad Request) if: + - The email format is invalid + - The email parameter is missing + - The password parameter is missing (for QR code generation) + - The code parameter is missing (for validation) + - The code format is invalid + +In the use case layer, exceptions related with arguments will be `IllegalArgumentException`. However, in the RESTful Web Service layer these will be transformed to Bad Request (HTTP 400) or Unauthorized (HTTP 401) as appropriate. + +## Using 2FA with Login + +When a user has 2FA enabled and `require2FAForBasicLogin` is set to `true`: + +1. **Initial Login**: User attempts login via `/users/login` with email and password +2. **2FA Required**: Service returns `LoginResponseDTO` with `requires2FA: true` +3. **Code Entry**: Client prompts user for 2FA code from authenticator app +4. **2FA Validation**: Client sends email and code to `/users/login/2fa` +5. **Success**: Service validates code and returns `LoginResponseDTO` with JWT token + +**Example Flow:** + +```shell +# Step 1: Initial login +curl -X POST \ + 'http://localhost:8080/users/login' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'email=orion@test.com' \ + --data-urlencode 'password=12345678' + +# Response indicates 2FA is required: +# { +# "authentication": null, +# "requires2FA": true, +# "message": "2FA code required" +# } + +# Step 2: Validate 2FA code +curl -X POST \ + 'http://localhost:8080/users/login/2fa' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'email=orion@test.com' \ + --data-urlencode 'code=123456' + +# Response contains JWT token: +# { +# "authentication": { +# "user": {...}, +# "token": "eyJ..." +# }, +# "requires2FA": false, +# "message": null +# } +``` + +## Supported Authenticator Apps + +The generated QR codes are compatible with any TOTP-compatible authenticator app, including: + +- Google Authenticator +- Microsoft Authenticator +- Authy +- 1Password +- LastPass Authenticator +- Any other TOTP-compatible app diff --git a/docs/usecases/UpdateUser/updateUser.md b/docs/usecases/UpdateUser/updateUser.md index f312bf8..fdebdda 100644 --- a/docs/usecases/UpdateUser/updateUser.md +++ b/docs/usecases/UpdateUser/updateUser.md @@ -5,13 +5,66 @@ parent: Use Cases nav_order: 7 --- -## Normal flow +## Update User -* A client sends the current e-mail, optionally the new e-mail and/or the current and new password along with an access token. +This use case is responsible for updating user information (email and/or password) in the system. The endpoint requires JWT authentication and returns a `LoginResponseDTO` containing the updated user data and a new JWT token. + +### Normal flow + +* A client sends the current e-mail, optionally the new e-mail and/or the current and new password along with a valid JWT token. +* The service validates the JWT token and extracts the user's email from the token. * The service validates the access token and the current e-mail to check if the user exists in the service. -* If updating email: If the user exists, updates the user's e-mail, sets the status of e-mail validation to false, sends a message with a code to validate the new e-mail, and generates a new access token. -* If updating password: If the user exists, validates the current password and if it matches, updates the user's password and generates a new access token. -* The service returns a LoginResponseDTO containing an AuthenticationDTO with the new JWT token and updated user information. +* **If updating email:** + * If the user exists, the service updates the user's e-mail + * Sets the status of e-mail validation to `false` + * Sends a message with a code to validate the new e-mail + * Generates a new access token +* **If updating password:** + * If the user exists, the service validates the current password + * If the current password matches, updates the user's password + * Generates a new access token +* The service returns a `LoginResponseDTO` containing an `AuthenticationDTO` with the new JWT token and updated user information. + +### LoginResponseDTO + +The `/users/update` endpoint returns a `LoginResponseDTO` object containing the updated user data and a new JWT token: + +**Structure:** + +```json +{ + "authentication": { + "user": { + "hash": "string", + "name": "string", + "email": "string", + "emailValid": boolean, + "secret2FA": "string | null", + "using2FA": boolean + }, + "token": "string (JWT)" + }, + "requires2FA": boolean, + "message": "string | null" +} +``` + +**Fields:** + +- `authentication` (AuthenticationDTO): Contains the updated user data and new JWT token +- `requires2FA` (boolean): Always `false` for update operations +- `message` (string | null): Typically `null` for successful updates + +**AuthenticationDTO Structure:** + +- `user` (UserEntity): The updated user object containing: + - `hash`: Unique identifier for the user + - `name`: User's display name + - `email`: User's email address (updated if `newEmail` was provided) + - `emailValid`: Whether the email has been validated (set to `false` if email was updated) + - `secret2FA`: The 2FA secret (null if not configured) + - `using2FA`: Whether 2FA is enabled for this user +- `token` (string): A new signed JWT token that should be used for subsequent authenticated requests ## HTTPS endpoints @@ -20,78 +73,91 @@ nav_order: 7 * Consumes: application/x-www-form-urlencoded * Produces: application/json * Requires: JWT token in Authorization header - * Examples: - - * Example of request to update email only: - - ```shell - curl -X PUT \ - 'http://localhost:8080/users/update' \ - --header 'Accept: */*' \ - --header 'Authorization: Bearer eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.UbrbrSkwKUOPm12kdcrbwroXe8cwPRg9tLN0ovxEB89bm9LNPYTj6qATOAHrObG-jDf2CUK1m3C5cTZE5LAx-hFt7YgdjXC4qxx57GvyzNY6Dxg_lp-pM3fEvw3CBQujRD47Lr3KwjPilSrwhAvXv46mw78cPHkw3kuNerdNf00TyIBYFobKezFxqT-jTDAPmzFo4B2jwFDtzOKf61Jq30E8i6H8IIDQ7XohbGdotbqu6RdR5uzoaJqB8ylz1hNXGWaBFD3GrsyCeFX9G6zgs998BoIhceKOksEZYhR7TD9q8SIuZWQTeIcgtuLBNMeQ7PV04bvZAyNCcN45sD7QJw.rwzVRmyTL16f1s9i.JrGJwG-ANTLqKuaEFTk-IEO5sdhthG9Z5-DXcli39X3mJKPn2gYIZyO4yp6VlFVw_gRT7x_YwwnLM0UuTqfpdL7fgSCS20nhC1fJYd0fvZCPLHBtJUDdo7gkswmG6eb4M1QENDkK96biwRGq0sCG5dZPfkDp1PXJQyF63phEPEb9Bo6xVzoJjJl-9C25BQRRSBHrGzp9tzY3Bh2WMv1JGJGG2thhuh2L6ap4QvM1jdyS9ObeNL61al_4UIk8CesZ0a5enheICPaL5YfbMHpwCWd3PutmABr-o05jtw.7dE9ieDGe3qOooGWmR-jJg' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'email=orion@test.com' \ - --data-urlencode 'newEmail=orion@xyzmail.com' - ``` - - * Example of request to update password only: - - ```shell - curl -X PUT \ - 'http://localhost:8080/users/update' \ - --header 'Accept: */*' \ - --header 'Authorization: Bearer eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.UbrbrSkwKUOPm12kdcrbwroXe8cwPRg9tLN0ovxEB89bm9LNPYTj6qATOAHrObG-jDf2CUK1m3C5cTZE5LAx-hFt7YgdjXC4qxx57GvyzNY6Dxg_lp-pM3fEvw3CBQujRD47Lr3KwjPilSrwhAvXv46mw78cPHkw3kuNerdNf00TyIBYFobKezFxqT-jTDAPmzFo4B2jwFDtzOKf61Jq30E8i6H8IIDQ7XohbGdotbqu6RdR5uzoaJqB8ylz1hNXGWaBFD3GrsyCeFX9G6zgs998BoIhceKOksEZYhR7TD9q8SIuZWQTeIcgtuLBNMeQ7PV04bvZAyNCcN45sD7QJw.rwzVRmyTL16f1s9i.JrGJwG-ANTLqKuaEFTk-IEO5sdhthG9Z5-DXcli39X3mJKPn2gYIZyO4yp6VlFVw_gRT7x_YwwnLM0UuTqfpdL7fgSCS20nhC1fJYd0fvZCPLHBtJUDdo7gkswmG6eb4M1QENDkK96biwRGq0sCG5dZPfkDp1PXJQyF63phEPEb9Bo6xVzoJjJl-9C25BQRRSBHrGzp9tzY3Bh2WMv1JGJGG2thhuh2L6ap4QvM1jdyS9ObeNL61al_4UIk8CesZ0a5enheICPaL5YfbMHpwCWd3PutmABr-o05jtw.7dE9ieDGe3qOooGWmR-jJg' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'email=orion@test.com' \ - --data-urlencode 'password=12345678' \ - --data-urlencode 'newPassword=87654321' - ``` - - * Example of request to update both email and password: - - ```shell - curl -X PUT \ - 'http://localhost:8080/users/update' \ - --header 'Accept: */*' \ - --header 'Authorization: Bearer eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.UbrbrSkwKUOPm12kdcrbwroXe8cwPRg9tLN0ovxEB89bm9LNPYTj6qATOAHrObG-jDf2CUK1m3C5cTZE5LAx-hFt7YgdjXC4qxx57GvyzNY6Dxg_lp-pM3fEvw3CBQujRD47Lr3KwjPilSrwhAvXv46mw78cPHkw3kuNerdNf00TyIBYFobKezFxqT-jTDAPmzFo4B2jwFDtzOKf61Jq30E8i6H8IIDQ7XohbGdotbqu6RdR5uzoaJqB8ylz1hNXGWaBFD3GrsyCeFX9G6zgs998BoIhceKOksEZYhR7TD9q8SIuZWQTeIcgtuLBNMeQ7PV04bvZAyNCcN45sD7QJw.rwzVRmyTL16f1s9i.JrGJwG-ANTLqKuaEFTk-IEO5sdhthG9Z5-DXcli39X3mJKPn2gYIZyO4yp6VlFVw_gRT7x_YwwnLM0UuTqfpdL7fgSCS20nhC1fJYd0fvZCPLHBtJUDdo7gkswmG6eb4M1QENDkK96biwRGq0sCG5dZPfkDp1PXJQyF63phEPEb9Bo6xVzoJjJl-9C25BQRRSBHrGzp9tzY3Bh2WMv1JGJGG2thhuh2L6ap4QvM1jdyS9ObeNL61al_4UIk8CesZ0a5enheICPaL5YfbMHpwCWd3PutmABr-o05jtw.7dE9ieDGe3qOooGWmR-jJg' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'email=orion@test.com' \ - --data-urlencode 'newEmail=orion@xyzmail.com' \ - --data-urlencode 'password=12345678' \ - --data-urlencode 'newPassword=87654321' - ``` - - * Example of response: LoginResponseDTO with AuthenticationDTO in JSON. - - ```json - { - "authentication": { - "token": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.k3VZmBKoYebrmPQcV5vVrNG1d1s-Ee4Szjh--iUwHClWzOLZfHWBRNHAcp70IS7VZM6JcAtVqXmLHP9quaR3OxSpUAAcgxnG-zIt6ogkd9vxiCttgwNGAqnd4pWUZ9ie4AWi9S-subt5KXDQ41kEuMLMJ2ufHLc4yU7XmKm5rkEWwXTjmmJCfb-soreb1bUpZ-SfoQ3zVX9MWoHYInnjzyZYLUfQIq0JfZZhKx4v689aE27nCek5iol-42LsQzowOTa9kvzxbN9ZofP_mVSuuXNJk7lTTZqX8ZU-BlwA27_W0t0sDj3Ka8H2GYyqAIBbUcWc_MdeHDnUQWeAMF57Aw.LPYiVFh9FxVW2D57.JwHCxJsICElkF85gTBpgX1fOirjFohzWGFeozzfjuyrrC_PJJhzHIR1tsZ6lfQi7jrjHeCT-aRjOW2r-U-baEbkguEzCYyG68ynFjjU65kajeoKSgoI4SVgdByK_bnHGhv-CTUzv4d4gD0Jt0OYw9H9a5QvozA9r_RiRdF-WwEYoyYSlvIxzxx3hlL07tbYO6z_dcEcd_-Y3ylKooRSXsoG_FSd6IzuJqlD10Ixax1uL-bmap2rUEqMjpcnIcMiyL9nF_-PhAjC7FnhCWJUtkj9NGzxPxZqiak-Wc8c2SdXf0vRKaiL72MkIxRo.1IQPzuVpukQwyqBA9S0rZA", - "user": { - "hash": "7eba8ef2-426b-446a-9f05-4ab67e71383d", - "name": "Orion", - "email": "orion@xyzmail.com", - "emailValid": false - } - }, - "requires2FA": false, - "message": null - } - ``` + +### Request Examples + +#### Example 1: Update Email Only + +```shell +curl -X PUT \ + 'http://localhost:8080/users/update' \ + --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'email=orion@test.com' \ + --data-urlencode 'newEmail=orion@xyzmail.com' +``` + +#### Example 2: Update Password Only + +```shell +curl -X PUT \ + 'http://localhost:8080/users/update' \ + --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'email=orion@test.com' \ + --data-urlencode 'password=12345678' \ + --data-urlencode 'newPassword=87654321' +``` + +#### Example 3: Update Both Email and Password + +```shell +curl -X PUT \ + 'http://localhost:8080/users/update' \ + --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'email=orion@test.com' \ + --data-urlencode 'newEmail=orion@xyzmail.com' \ + --data-urlencode 'password=12345678' \ + --data-urlencode 'newPassword=87654321' +``` + +### Response Example + +When the user is successfully updated, the response contains the updated user data and a new JWT token: + +```json +{ + "authentication": { + "token": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ...", + "user": { + "hash": "7eba8ef2-426b-446a-9f05-4ab67e71383d", + "name": "Orion", + "email": "orion@xyzmail.com", + "emailValid": false, + "secret2FA": null, + "using2FA": false + } + }, + "requires2FA": false, + "message": null +} +``` + +**Note:** After updating the email, `emailValid` is set to `false` and a validation email is sent to the new email address. The user must validate the new email using the `/users/validateEmail` endpoint. ## Parameters -* `email` (required): The current email of the user +* `email` (required): The current email of the user (must match the email in the JWT token) * `newEmail` (optional): The new email address. If provided, the email will be updated and a validation email will be sent. * `password` (optional): The current password. Required if `newPassword` is provided. -* `newPassword` (optional): The new password. Must meet password strength requirements. +* `newPassword` (optional): The new password. Must meet password strength requirements (minimum 8 characters). **Note:** At least one of `newEmail` or `newPassword` must be provided. ## Exceptions -In the use case layer, exceptions related with arguments will be -IllegalArgumentException. However, in the RESTful Web Service layer will be -transformed to Bad Request (HTTP 400). If the access token is invalid or missing, -the service will return Unauthorized (HTTP 401). - +RESTful Web Service layer will return: +- HTTP 401 (Unauthorized) if: + - The JWT token is missing or invalid + - The email in the request does not match the email in the JWT token + - The current password is incorrect (when updating password) +- HTTP 400 (Bad Request) if: + - The email format is invalid + - The new email already exists in the system + - The password does not meet the requirements + - Both `newEmail` and `newPassword` are missing + - `newPassword` is provided but `password` is missing + - Any required parameter is missing + +In the use case layer, exceptions related with arguments will be `IllegalArgumentException`. However, in the RESTful Web Service layer these will be transformed to Bad Request (HTTP 400) or Unauthorized (HTTP 401) as appropriate. diff --git a/docs/usecases/ValidateEmail/validateEmail.md b/docs/usecases/ValidateEmail/validateEmail.md index 7f4c907..3bafa3a 100644 --- a/docs/usecases/ValidateEmail/validateEmail.md +++ b/docs/usecases/ValidateEmail/validateEmail.md @@ -1,17 +1,21 @@ --- layout: default -title: Validade E-mail +title: Validate Email parent: Use Cases nav_order: 4 --- -## Validade E-mail +## Validate Email + +This use case is responsible for validating a user's email address using a validation code sent to the user's email. The endpoint verifies the code and marks the email as validated in the system. ### Normal flow -* A client sends the validation code and e-mail. -* The service validates the code to the e-mail. -* If the validation code is correct, the service returns just a string true. +* A client sends the validation code and e-mail as query parameters. +* The service validates the input data and verifies if the user exists in the system. +* The service checks if the validation code matches the code sent to the user's email. +* If the validation code is correct and the user exists, the service marks the email as validated (`emailValid` set to `true`). +* The service returns a boolean `true` value indicating successful validation. ## HTTPS endpoints @@ -19,24 +23,33 @@ nav_order: 4 * Method: GET * Consumes: text/plain * Produces: text/plain - * Examples: - * Example of request: +### Request Example + +```shell +curl -X GET \ + 'http://localhost:8080/users/validateEmail?code=d32c2a8e-ea4b-4260-b4d7-b3e62d8488e1&email=orion%40test.com' \ + --header 'Accept: text/plain' +``` - ```shell - curl -X 'GET' \ - 'http://localhost:8080/users/validateEmail?code=d32c2a8e-ea4b-4260-b4d7-b3e62d8488e1&email=orion%40test.com' \ - -H 'accept: application/json' - ``` +### Response Example - * Example of response: +When the email is successfully validated, the response contains: - ```txt - true - ``` +```txt +true +``` + +**Note:** The endpoint returns HTTP 200 (OK) with a plain text response containing the boolean value `true` indicating successful validation. ## Exceptions -In the use case layer, exceptions related with arguments will be -IllegalArgumentException. However, in the RESTful Web Service layer will be -transformed to Bad Request (HTTP 400). +RESTful Web Service layer will return a HTTP 400 (Bad Request) if: +- The email format is invalid +- The email parameter is missing +- The code parameter is missing +- The validation code is incorrect or expired +- The user does not exist in the system +- The email has already been validated + +In the use case layer, exceptions related with arguments will be `IllegalArgumentException`. However, in the RESTful Web Service layer these will be transformed to Bad Request (HTTP 400). diff --git a/docs/usuario/2FA.md b/docs/usuario/2FA.md deleted file mode 100644 index f1f931e..0000000 --- a/docs/usuario/2FA.md +++ /dev/null @@ -1,153 +0,0 @@ ---- -layout: default -title: Autenticação em Dois Fatores (2FA) -parent: Documentação do Usuário -nav_order: 1 ---- - -# Autenticação em Dois Fatores (2FA) - -## O que é 2FA? - -A Autenticação em Dois Fatores (2FA) adiciona uma camada extra de segurança à sua conta. Além da sua senha, você precisará fornecer um código de verificação gerado por um aplicativo autenticador no seu dispositivo móvel. - -## Como Funciona? - -Quando você ativa o 2FA, o sistema gera um código QR que você escaneia com um aplicativo autenticador (como Google Authenticator, Microsoft Authenticator, ou Authy). A partir desse momento, sempre que você fizer login, além da sua senha, você precisará informar o código de 6 dígitos gerado pelo aplicativo. - -## Como Ativar o 2FA - -### Passo 1: Fazer Login - -Primeiro, faça login na sua conta usando seu email e senha normalmente. - -### Passo 2: Gerar o Código QR - -Envie uma requisição POST para o endpoint `/users/google/2FAuth/qrCode` com suas credenciais: - -```bash -curl -X POST \ - 'http://localhost:8080/users/google/2FAuth/qrCode' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'email=seu@email.com' \ - --data-urlencode 'password=suasenha' -``` - -A resposta será uma imagem PNG contendo o código QR. - -### Passo 3: Escanear o Código QR - -1. Abra o aplicativo autenticador no seu dispositivo móvel (Google Authenticator, Microsoft Authenticator, etc.) -2. Toque em "Adicionar conta" ou o botão "+" -3. Escolha "Escanear código QR" -4. Escaneie o código QR recebido na resposta da API -5. O aplicativo começará a gerar códigos de 6 dígitos que mudam a cada 30 segundos - -### Passo 4: Validar a Configuração - -Para confirmar que o 2FA está configurado corretamente, valide um código: - -```bash -curl -X POST \ - 'http://localhost:8080/users/google/2FAuth/validate' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'email=seu@email.com' \ - --data-urlencode 'code=123456' -``` - -Substitua `123456` pelo código atual exibido no seu aplicativo autenticador. - -Se a validação for bem-sucedida, você receberá um token JWT, confirmando que o 2FA está ativo. - -## Como Usar o 2FA no Login - -### Login Normal (sem 2FA) - -Se você não tiver 2FA ativado, o login funciona normalmente: - -```bash -curl -X POST \ - 'http://localhost:8080/users/login' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'email=seu@email.com' \ - --data-urlencode 'password=suasenha' -``` - -### Login com 2FA Ativado - -Se você tiver 2FA ativado, o processo é em duas etapas: - -**Etapa 1: Login Inicial** - -```bash -curl -X POST \ - 'http://localhost:8080/users/login' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'email=seu@email.com' \ - --data-urlencode 'password=suasenha' -``` - -A resposta indicará que o código 2FA é necessário: - -```json -{ - "requires2FA": true, - "message": "2FA code required" -} -``` - -**Etapa 2: Validar Código 2FA** - -Use o endpoint `/users/login/2fa` para completar a autenticação: - -```bash -curl -X POST \ - 'http://localhost:8080/users/login/2fa' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'email=seu@email.com' \ - --data-urlencode 'code=123456' -``` - -Substitua `123456` pelo código atual do seu aplicativo autenticador. - -Se o código estiver correto, você receberá o token JWT de autenticação. - -## Como Desativar o 2FA - -Atualmente, a desativação do 2FA requer contato com o suporte ou acesso direto ao banco de dados. Em versões futuras, será possível desativar através da interface do usuário. - -## Solução de Problemas - -### O código não está funcionando - -1. **Verifique a hora do dispositivo**: Os códigos TOTP dependem do tempo sincronizado. Certifique-se de que o relógio do seu dispositivo está correto. - -2. **Use o código mais recente**: Os códigos mudam a cada 30 segundos. Certifique-se de usar o código atual exibido no aplicativo. - -3. **Verifique se o 2FA está ativado**: Confirme que você completou o processo de ativação corretamente. - -### Perdi acesso ao aplicativo autenticador - -Se você perdeu acesso ao aplicativo autenticador e não tem códigos de backup, entre em contato com o suporte para recuperar o acesso à sua conta. - -### O QR Code não escaneia - -1. Certifique-se de que a imagem está nítida -2. Tente aumentar o brilho da tela -3. Verifique se o aplicativo autenticador tem permissão para usar a câmera -4. Tente inserir manualmente a chave secreta (se disponível) - -## Segurança - -- **Mantenha seu dispositivo seguro**: O aplicativo autenticador deve estar protegido com senha ou biometria -- **Não compartilhe códigos**: Nunca compartilhe códigos 2FA com outras pessoas -- **Use códigos de backup**: Alguns aplicativos permitem gerar códigos de backup - guarde-os em local seguro -- **Notifique sobre atividade suspeita**: Se receber códigos 2FA sem ter solicitado login, sua conta pode estar comprometida - -## Aplicativos Recomendados - -- **Google Authenticator**: Disponível para iOS e Android -- **Microsoft Authenticator**: Disponível para iOS e Android -- **Authy**: Disponível para iOS, Android e Desktop -- **1Password**: Inclui autenticador integrado - diff --git a/docs/usuario/WebAuthn.md b/docs/usuario/WebAuthn.md deleted file mode 100644 index e2a5c91..0000000 --- a/docs/usuario/WebAuthn.md +++ /dev/null @@ -1,279 +0,0 @@ ---- -layout: default -title: WebAuthn (Autenticação sem Senha) -parent: Documentação do Usuário -nav_order: 2 ---- - -# WebAuthn (Autenticação sem Senha) - -## O que é WebAuthn? - -WebAuthn é um padrão de autenticação que permite fazer login sem usar senhas tradicionais. Em vez disso, você usa dispositivos de segurança físicos (como chaves de segurança USB) ou recursos biométricos do seu dispositivo (como impressão digital ou reconhecimento facial). - -## Dispositivos Suportados - -### Chaves de Segurança FIDO2 - -- **YubiKey**: Chaves de segurança USB e NFC -- **Google Titan**: Chaves de segurança USB e Bluetooth -- **Feitian**: Várias opções de chaves de segurança -- Qualquer dispositivo compatível com FIDO2/WebAuthn - -### Biometria - -- **Windows Hello**: Impressão digital e reconhecimento facial no Windows -- **Touch ID**: Impressão digital em dispositivos Apple -- **Face ID**: Reconhecimento facial em dispositivos Apple -- **Android Biometric**: Impressão digital e reconhecimento facial em Android - -## Como Registrar um Dispositivo WebAuthn - -### Passo 1: Iniciar o Registro - -Envie uma requisição POST para iniciar o processo de registro: - -```bash -curl -X POST \ - 'http://localhost:8080/users/webauthn/register/start' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'email=seu@email.com' -``` - -A resposta contém as opções de registro (PublicKeyCredentialCreationOptions) que serão usadas pelo navegador para criar a credencial. - -### Passo 2: Criar a Credencial no Navegador - -No seu aplicativo frontend, use a API WebAuthn do navegador para criar a credencial: - -```javascript -// Parse as opções recebidas do servidor -const options = JSON.parse(response.options); - -// Converter challenge de base64url para ArrayBuffer -const challenge = base64urlToArrayBuffer(options.challenge); - -// Criar a credencial -const credential = await navigator.credentials.create({ - publicKey: { - ...options, - challenge: challenge - } -}); -``` - -### Passo 3: Finalizar o Registro - -Envie a resposta da credencial para o servidor: - -```bash -curl -X POST \ - 'http://localhost:8080/users/webauthn/register/finish' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'email=seu@email.com' \ - --data-urlencode 'response=' \ - --data-urlencode 'deviceName=Meu Dispositivo' -``` - -O `deviceName` é opcional e ajuda você a identificar o dispositivo registrado. - -## Como Autenticar com WebAuthn - -### Passo 1: Iniciar a Autenticação - -Envie uma requisição POST para iniciar o processo de autenticação: - -```bash -curl -X POST \ - 'http://localhost:8080/users/webauthn/authenticate/start' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'email=seu@email.com' -``` - -A resposta contém as opções de autenticação (PublicKeyCredentialRequestOptions). - -### Passo 2: Autenticar no Navegador - -No seu aplicativo frontend, use a API WebAuthn do navegador: - -```javascript -// Parse as opções recebidas do servidor -const options = JSON.parse(response.options); - -// Converter challenge de base64url para ArrayBuffer -const challenge = base64urlToArrayBuffer(options.challenge); - -// Obter a credencial -const assertion = await navigator.credentials.get({ - publicKey: { - ...options, - challenge: challenge - } -}); -``` - -### Passo 3: Finalizar a Autenticação - -Envie a resposta da autenticação para o servidor: - -```bash -curl -X POST \ - 'http://localhost:8080/users/webauthn/authenticate/finish' \ - --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'email=seu@email.com' \ - --data-urlencode 'response=' -``` - -Se a autenticação for bem-sucedida, você receberá um token JWT. - -## Como Remover um Dispositivo - -Atualmente, a remoção de dispositivos WebAuthn requer contato com o suporte ou acesso direto ao banco de dados. Em versões futuras, será possível gerenciar dispositivos através da interface do usuário. - -## Requisitos Técnicos - -### Navegadores Suportados - -- **Chrome**: Versão 67+ -- **Firefox**: Versão 60+ -- **Safari**: Versão 13+ -- **Edge**: Versão 18+ - -### HTTPS Obrigatório - -O WebAuthn requer conexão HTTPS para funcionar. Em desenvolvimento local, você pode usar `https://localhost` com certificado auto-assinado. - -### Domínio Configurado - -O domínio deve estar configurado corretamente no servidor. Para desenvolvimento local, use `localhost`. - -## Solução de Problemas - -### "NotSupportedError" no navegador - -1. **Verifique o navegador**: Certifique-se de estar usando um navegador compatível com WebAuthn -2. **Verifique HTTPS**: WebAuthn só funciona em HTTPS (ou localhost) -3. **Verifique o dispositivo**: Certifique-se de que seu dispositivo suporta WebAuthn - -### A chave de segurança não é reconhecida - -1. **Verifique a conexão**: Certifique-se de que a chave está conectada corretamente -2. **Tente outra porta USB**: Algumas chaves funcionam melhor em portas USB 2.0 -3. **Verifique os drivers**: No Windows, pode ser necessário instalar drivers específicos -4. **Teste em outro navegador**: Alguns navegadores têm melhor suporte que outros - -### Biometria não funciona - -1. **Verifique as configurações**: Certifique-se de que a biometria está configurada no dispositivo -2. **Verifique as permissões**: O navegador precisa ter permissão para acessar a biometria -3. **Tente outro método**: Se Face ID não funcionar, tente Touch ID ou vice-versa - -### "Invalid credential" durante autenticação - -1. **Verifique o email**: Certifique-se de estar usando o mesmo email usado no registro -2. **Verifique o dispositivo**: Use o mesmo dispositivo ou chave de segurança usada no registro -3. **Verifique se o dispositivo está registrado**: Confirme que você completou o processo de registro - -## Segurança - -- **Proteja seu dispositivo**: Mantenha seu dispositivo físico seguro -- **Use chaves de segurança**: Chaves de segurança físicas são mais seguras que biometria -- **Tenha dispositivos de backup**: Registre múltiplos dispositivos para evitar perda de acesso -- **Notifique sobre atividade suspeita**: Se receber solicitações de autenticação WebAuthn sem ter solicitado, sua conta pode estar comprometida - -## Vantagens do WebAuthn - -- **Sem senhas**: Não precisa lembrar ou gerenciar senhas -- **Mais seguro**: Resistant a phishing e ataques de força bruta -- **Mais rápido**: Autenticação rápida com biometria ou chave de segurança -- **Padrão aberto**: Suportado por todos os principais navegadores e plataformas - -## Exemplo Completo (JavaScript) - -```javascript -// Função auxiliar para converter base64url para ArrayBuffer -function base64urlToArrayBuffer(base64url) { - const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/'); - const binary = atob(base64); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - return bytes.buffer; -} - -// Função auxiliar para converter ArrayBuffer para base64url -function arrayBufferToBase64url(buffer) { - const bytes = new Uint8Array(buffer); - let binary = ''; - for (let i = 0; i < bytes.length; i++) { - binary += String.fromCharCode(bytes[i]); - } - const base64 = btoa(binary); - return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); -} - -// Registrar dispositivo -async function registerWebAuthn(email) { - // 1. Iniciar registro - const startResponse = await fetch('/users/webauthn/register/start', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ email }) - }); - const startData = await startResponse.json(); - const options = JSON.parse(startData.options); - - // 2. Criar credencial - const publicKey = { - ...options, - challenge: base64urlToArrayBuffer(options.challenge) - }; - const credential = await navigator.credentials.create({ publicKey }); - - // 3. Finalizar registro - const finishResponse = await fetch('/users/webauthn/register/finish', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - email, - response: JSON.stringify(credential), - deviceName: 'Meu Dispositivo' - }) - }); - - return await finishResponse.json(); -} - -// Autenticar com WebAuthn -async function authenticateWebAuthn(email) { - // 1. Iniciar autenticação - const startResponse = await fetch('/users/webauthn/authenticate/start', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ email }) - }); - const startData = await startResponse.json(); - const options = JSON.parse(startData.options); - - // 2. Obter credencial - const publicKey = { - ...options, - challenge: base64urlToArrayBuffer(options.challenge) - }; - const assertion = await navigator.credentials.get({ publicKey }); - - // 3. Finalizar autenticação - const finishResponse = await fetch('/users/webauthn/authenticate/finish', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - email, - response: JSON.stringify(assertion) - }) - }); - - return await finishResponse.json(); -} -``` - diff --git a/docs/usuario/index.md b/docs/usuario/index.md deleted file mode 100644 index 06f46ff..0000000 --- a/docs/usuario/index.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -layout: default -title: Documentação do Usuário -nav_order: 1 ---- - -# Documentação do Usuário - -Bem-vindo à documentação do usuário do Orion Users! Esta seção contém guias detalhados sobre como usar as funcionalidades de segurança avançada disponíveis no serviço. - -## Funcionalidades Disponíveis - -### Autenticação em Dois Fatores (2FA) - -A autenticação em dois fatores adiciona uma camada extra de segurança à sua conta usando códigos TOTP gerados por aplicativos autenticadores. - -[**Guia Completo de 2FA →**](2FA.md) - -**Recursos:** -- Geração de código QR para configuração -- Suporte a aplicativos autenticadores populares (Google Authenticator, Microsoft Authenticator, etc.) -- Integração com o fluxo de login existente -- Validação de códigos TOTP - -### WebAuthn (Autenticação sem Senha) - -O WebAuthn permite autenticação sem senhas usando chaves de segurança físicas ou biometria do dispositivo. - -[**Guia Completo de WebAuthn →**](WebAuthn.md) - -**Recursos:** -- Suporte a chaves de segurança FIDO2 -- Autenticação biométrica (impressão digital, reconhecimento facial) -- Registro e gerenciamento de múltiplos dispositivos -- Autenticação rápida e segura - -## Início Rápido - -### Ativar 2FA - -1. Faça login na sua conta -2. Gere um código QR através do endpoint `/users/google/2FAuth/qrCode` -3. Escaneie o código com um aplicativo autenticador -4. Valide a configuração com um código TOTP - -### Registrar Dispositivo WebAuthn - -1. Inicie o registro através do endpoint `/users/webauthn/register/start` -2. Use a API WebAuthn do navegador para criar a credencial -3. Finalize o registro através do endpoint `/users/webauthn/register/finish` - -## Segurança - -Ambas as funcionalidades (2FA e WebAuthn) foram projetadas para aumentar significativamente a segurança da sua conta: - -- **2FA**: Adiciona uma segunda camada de autenticação usando algo que você possui (seu dispositivo móvel) -- **WebAuthn**: Elimina a necessidade de senhas, usando criptografia de chave pública e dispositivos físicos ou biometria - -## Suporte - -Se você encontrar problemas ou tiver dúvidas: - -1. Consulte a seção "Solução de Problemas" em cada guia -2. Verifique os requisitos técnicos -3. Entre em contato com o suporte se necessário - -## Próximos Passos - -- [Configurar 2FA](2FA.md) -- [Configurar WebAuthn](WebAuthn.md) -- [Documentação da API](../usecases/usecases.md) - From da2cb493c9da633b14f2881fad956a276807ba16 Mon Sep 17 00:00:00 2001 From: Rodrigo Prestes Machado Date: Sun, 23 Nov 2025 21:32:49 -0300 Subject: [PATCH 4/4] Users Admin Console --- CHANGELOG.md | 5 +- README.md | 77 +- .../adapters/controllers/UserController.kt | 59 +- .../adapters/gateways/entities/UserEntity.kt | 7 + .../gateways/repository/UserRepository.kt | 7 + .../gateways/repository/UserRepositoryImpl.kt | 10 + .../application/interfaces/UpdateUser.kt | 5 +- .../application/usecases/UpdateUserImpl.kt | 17 +- .../users/frameworks/rest/users/UserWS.kt | 92 +- .../resources/META-INF/resources/.gitignore | 3 +- .../META-INF/resources/admin/.gitignore | 30 + .../META-INF/resources/admin/README.md | 87 + .../META-INF/resources/admin/index.html | 14 + .../resources/admin/package-lock.json | 1573 +++++++++++++++++ .../META-INF/resources/admin/package.json | 24 + .../META-INF/resources/admin/src/App.vue | 129 ++ .../admin/src/components/DeleteUserDialog.vue | 84 + .../META-INF/resources/admin/src/main.js | 26 + .../resources/admin/src/router/index.js | 122 ++ .../resources/admin/src/services/api.js | 111 ++ .../resources/admin/src/stores/users.js | 204 +++ .../admin/src/views/CreateUserView.vue | 194 ++ .../admin/src/views/EditUserView.vue | 262 +++ .../resources/admin/src/views/LoginView.vue | 160 ++ .../admin/src/views/UserDetailView.vue | 281 +++ .../admin/src/views/UsersListView.vue | 241 +++ .../META-INF/resources/admin/vite.config.js | 62 + .../playground/src/views/LoginView.vue | 10 +- src/main/resources/import.sql | 12 +- 29 files changed, 3875 insertions(+), 33 deletions(-) create mode 100644 src/main/resources/META-INF/resources/admin/.gitignore create mode 100644 src/main/resources/META-INF/resources/admin/README.md create mode 100644 src/main/resources/META-INF/resources/admin/index.html create mode 100644 src/main/resources/META-INF/resources/admin/package-lock.json create mode 100644 src/main/resources/META-INF/resources/admin/package.json create mode 100644 src/main/resources/META-INF/resources/admin/src/App.vue create mode 100644 src/main/resources/META-INF/resources/admin/src/components/DeleteUserDialog.vue create mode 100644 src/main/resources/META-INF/resources/admin/src/main.js create mode 100644 src/main/resources/META-INF/resources/admin/src/router/index.js create mode 100644 src/main/resources/META-INF/resources/admin/src/services/api.js create mode 100644 src/main/resources/META-INF/resources/admin/src/stores/users.js create mode 100644 src/main/resources/META-INF/resources/admin/src/views/CreateUserView.vue create mode 100644 src/main/resources/META-INF/resources/admin/src/views/EditUserView.vue create mode 100644 src/main/resources/META-INF/resources/admin/src/views/LoginView.vue create mode 100644 src/main/resources/META-INF/resources/admin/src/views/UserDetailView.vue create mode 100644 src/main/resources/META-INF/resources/admin/src/views/UsersListView.vue create mode 100644 src/main/resources/META-INF/resources/admin/vite.config.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 418458d..402d6f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,13 @@ ## 0.0.6 - LoginResponseDTO as a standard response object from all endpoints -- New documentation +- Users Admin Console Vue App (Experimental) +- Playground Vue App (Experimental) ## 0.0.5 - Port to Kotlin -- webauthn (experimental) +- webauthn (Experimental) ## 0.0.4 diff --git a/README.md b/README.md index 7b03c30..52f39e4 100755 --- a/README.md +++ b/README.md @@ -49,6 +49,24 @@ The playground includes: For detailed information about the playground, including how to run it in development and production modes, social login configuration, and user guide, see the [Playground Documentation](docs/playground/Playground.md). +## Admin Console + +The project includes a Vue 3 admin console application built with Vuetify that provides an administrative interface for managing users in the Orion Users service. + +**Access the admin console**: After starting the application, navigate to `http://localhost:8080/console` + +**Authentication**: The admin console requires authentication with a JWT token that includes the `admin` role. Only users with admin privileges can access this interface. + +The admin console includes: +- User authentication with admin role verification +- User listing with filters and search functionality +- User detail view with complete user information +- User creation (create new users) +- User editing (update email and password) +- User deletion (delete users with confirmation) + +For detailed information about the admin console, including development setup and features, see the [Admin Console README](src/main/resources/META-INF/resources/admin/README.md). + ## Packaging and running the application The application can be packaged using: @@ -87,33 +105,65 @@ If you want to learn more about building native executables, please consult http ## API Endpoints -The service provides the following main endpoints: +The service provides the following endpoints: + +### User Management + +- `POST /users/create` - Create a new user. Returns user data in JSON format. +- `POST /users/createAuthenticate` - Create a new user and authenticate in a single request. Returns LoginResponseDTO with JWT token. +- `PUT /users/update` - Update user information (name, email and/or password). Requires JWT authentication (role: user). Returns LoginResponseDTO with updated token and user. Admins can update any user. +- `POST /users/delete` - Delete a user. Requires JWT authentication (role: admin). +- `GET /users/list` - List all users. Requires JWT authentication (role: admin). +- `GET /users/by-email` - Get user by email. Requires JWT authentication (role: admin). + +### Authentication + +- `POST /users/login` - Authenticate a user with email and password. Returns LoginResponseDTO. If user has 2FA enabled and `require2FAForBasicLogin` is true, returns `requires2FA: true` indicating that 2FA code is required. +- `POST /users/login/2fa` - Authenticate with 2FA code after initial login. Returns LoginResponseDTO with JWT token. +- `POST /users/authenticate` - **(Deprecated)** Authenticate a user. Returns JWT token as plain text. Use `/users/login` instead. + +### Email Validation + +- `GET /users/validateEmail` - Validate user email with validation code sent via email. Query parameters: `email` and `code`. + +### Password Recovery + +- `POST /users/recoverPassword` - Recover user password. Generates a new password and sends it via email. Returns HTTP 204 (No Content). + +### Social Authentication + +- `POST /users/login/google` - Authenticate with Google OAuth2. Accepts Google ID token or access token. Returns LoginResponseDTO. If user has 2FA enabled and `require2FAForSocialLogin` is true, returns `requires2FA: true`. +- `POST /users/login/google/2fa` - Complete social authentication with 2FA code. Returns LoginResponseDTO with JWT token. + +### Two-Factor Authentication (2FA) + +- `POST /users/google/2FAuth/qrCode` - Generate QR code for 2FA setup. Requires email and password. Returns PNG image. +- `POST /users/google/2FAuth/validate` - Validate 2FA TOTP code (standalone validation). Returns LoginResponseDTO with JWT token. +- `POST /users/2fa/settings` - Update 2FA settings for a user. Requires JWT authentication (role: user). Parameters: `email`, `require2FAForBasicLogin` (optional), `require2FAForSocialLogin` (optional). + +### WebAuthn (Biometric/Security Key Authentication) -- `POST /users/create` - Create a new user -- `POST /users/login` - Authenticate a user (returns LoginResponseDTO) -- `PUT /users/update` - Update user information (email and/or password). Requires JWT authentication. Returns LoginResponseDTO with updated token and user. -- `POST /users/delete` - Delete a user (admin only) -- `GET /users/validateEmail` - Validate user email with code -- `POST /users/google/2FAuth/qrCode` - Generate 2FA QR code -- `POST /users/google/2FAuth/validate` - Validate 2FA code -- `POST /users/login/2fa` - Login with 2FA code -- `POST /users/login/google` - Social authentication with Google +- `POST /users/webauthn/register/start` - Start WebAuthn registration process. Returns PublicKeyCredentialCreationOptions as JSON. +- `POST /users/webauthn/register/finish` - Finish WebAuthn registration. Saves credential for the user. +- `POST /users/webauthn/authenticate/start` - Start WebAuthn authentication process. Returns PublicKeyCredentialRequestOptions as JSON. +- `POST /users/webauthn/authenticate/finish` - Finish WebAuthn authentication. Returns LoginResponseDTO with JWT token. For complete API documentation, see the [documentation site](https://users.orion-services.dev). ## Update User Endpoint -The `/users/update` endpoint allows updating user email and/or password in a single request: +The `/users/update` endpoint allows updating user name, email and/or password in a single request: - **Method**: PUT -- **Authentication**: Required (JWT token in Authorization header) +- **Authentication**: Required (JWT token in Authorization header with role: user) - **Parameters**: - `email` (required): Current user email + - `name` (optional): New name - `newEmail` (optional): New email address - `password` (optional): Current password (required if updating password) - `newPassword` (optional): New password - **Response**: LoginResponseDTO containing AuthenticationDTO with new JWT token and updated user information -- **Note**: At least one of `newEmail` or `newPassword` must be provided +- **Note**: At least one of `name`, `newEmail` or `newPassword` must be provided. Admins can update any user; regular users can only update their own account. Example: ```bash @@ -121,6 +171,7 @@ curl -X PUT 'http://localhost:8080/users/update' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'email=user@example.com' \ + --data-urlencode 'name=New Name' \ --data-urlencode 'newEmail=newuser@example.com' ``` diff --git a/src/main/kotlin/dev/orion/users/adapters/controllers/UserController.kt b/src/main/kotlin/dev/orion/users/adapters/controllers/UserController.kt index 8ab0a4b..f140f93 100644 --- a/src/main/kotlin/dev/orion/users/adapters/controllers/UserController.kt +++ b/src/main/kotlin/dev/orion/users/adapters/controllers/UserController.kt @@ -630,10 +630,11 @@ class UserController : BasicController() { } /** - * Updates user information (email and/or password). Validates the token, + * Updates user information (name, email and/or password). Validates the token, * updates the fields, generates a new JWT, and sends a validation email if email was changed. * * @param email : The current email of the user + * @param name : The new name (optional) * @param newEmail : The new email address (optional) * @param password : The current password (required if updating password) * @param newPassword : The new password (optional) @@ -642,21 +643,27 @@ class UserController : BasicController() { */ fun updateUser( email: String, + name: String?, newEmail: String?, password: String?, newPassword: String?, - jwtEmail: String + jwtEmail: String, + isAdmin: Boolean = false ): Uni { // Validate using use case - val user: User = updateUserUC.updateUser(email, newEmail, password, newPassword) + val user: User = updateUserUC.updateUser(email, name, newEmail, password, newPassword) - // Validate that JWT email matches the current email - checkTokenEmail(email, jwtEmail) + // Validate that JWT email matches the current email (unless user is admin) + if (!isAdmin) { + checkTokenEmail(email, jwtEmail) + } // Capture variables for use in lambdas + val nameUpdated = !name.isNullOrBlank() val emailUpdated = !newEmail.isNullOrBlank() val passwordUpdate = !newPassword.isNullOrBlank() && !password.isNullOrBlank() val currentEmail = email + val newNameValue = name val newEmailValue = newEmail val currentPassword = password val newPasswordValue = newPassword @@ -674,6 +681,11 @@ class UserController : BasicController() { } } + // Update name if provided + if (nameUpdated) { + userEntity.name = newNameValue + } + // First update email if provided if (emailUpdated) { userRepository.updateEmail(currentEmail, newEmailValue!!) @@ -682,7 +694,21 @@ class UserController : BasicController() { sendValidationEmail(updatedUser) } } else { - Uni.createFrom().item(userEntity) + // If only name was updated, persist the user + if (nameUpdated) { + userRepository.updateUser(userEntity) + } else { + Uni.createFrom().item(userEntity) + } + } + } + .onItem().ifNotNull().transformToUni { updatedUser -> + // Update name if email was also updated (to ensure name is saved) + if (emailUpdated && nameUpdated) { + updatedUser.name = newNameValue + userRepository.updateUser(updatedUser) + } else { + Uni.createFrom().item(updatedUser) } } .onItem().ifNotNull().transformToUni { updatedUser -> @@ -765,5 +791,26 @@ class UserController : BasicController() { } } + /** + * Lists all users in the service. + * + * @return A Uni> containing all users + */ + fun listAllUsers(): Uni> { + return userRepository.listAllUsers() + } + + /** + * Gets a user by email. + * + * @param email The email of the user + * @return A Uni containing the user if found + */ + fun getUserByEmail(email: String): Uni { + return userRepository.findUserByEmail(email) + .onItem().ifNull() + .failWith(IllegalArgumentException("User not found")) + } + } diff --git a/src/main/kotlin/dev/orion/users/adapters/gateways/entities/UserEntity.kt b/src/main/kotlin/dev/orion/users/adapters/gateways/entities/UserEntity.kt index b298184..2253f97 100644 --- a/src/main/kotlin/dev/orion/users/adapters/gateways/entities/UserEntity.kt +++ b/src/main/kotlin/dev/orion/users/adapters/gateways/entities/UserEntity.kt @@ -23,6 +23,8 @@ import jakarta.persistence.Entity import jakarta.persistence.FetchType import jakarta.persistence.GeneratedValue import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.JoinTable import jakarta.persistence.ManyToMany import jakarta.persistence.Table import jakarta.validation.constraints.Email @@ -68,6 +70,11 @@ class UserEntity : PanacheEntityBase() { /** Role list. */ @JsonIgnore @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "User_Role", + joinColumns = [JoinColumn(name = "User_id")], + inverseJoinColumns = [JoinColumn(name = "roles_id")] + ) var roles: MutableList = mutableListOf() /** Stores if the e-mail was validated. */ diff --git a/src/main/kotlin/dev/orion/users/adapters/gateways/repository/UserRepository.kt b/src/main/kotlin/dev/orion/users/adapters/gateways/repository/UserRepository.kt index 3f1eb8e..9a7edb4 100644 --- a/src/main/kotlin/dev/orion/users/adapters/gateways/repository/UserRepository.kt +++ b/src/main/kotlin/dev/orion/users/adapters/gateways/repository/UserRepository.kt @@ -103,5 +103,12 @@ interface UserRepository : PanacheRepository { * @return Returns a Long 1 if user was deleted */ fun deleteUser(email: String): Uni + + /** + * Lists all users in the service. + * + * @return A Uni> containing all users + */ + fun listAllUsers(): Uni> } diff --git a/src/main/kotlin/dev/orion/users/adapters/gateways/repository/UserRepositoryImpl.kt b/src/main/kotlin/dev/orion/users/adapters/gateways/repository/UserRepositoryImpl.kt index f252116..e57955b 100644 --- a/src/main/kotlin/dev/orion/users/adapters/gateways/repository/UserRepositoryImpl.kt +++ b/src/main/kotlin/dev/orion/users/adapters/gateways/repository/UserRepositoryImpl.kt @@ -341,5 +341,15 @@ class UserRepositoryImpl @Inject constructor( return Panache.withTransaction { user.persist() } .onItem().transform { user } } + + /** + * Lists all users in the service. + * + * @return A Uni> containing all users + */ + override fun listAllUsers(): Uni> { + return (this as io.quarkus.hibernate.reactive.panache.PanacheRepository) + .listAll() + } } diff --git a/src/main/kotlin/dev/orion/users/application/interfaces/UpdateUser.kt b/src/main/kotlin/dev/orion/users/application/interfaces/UpdateUser.kt index 7939eea..d98f613 100644 --- a/src/main/kotlin/dev/orion/users/application/interfaces/UpdateUser.kt +++ b/src/main/kotlin/dev/orion/users/application/interfaces/UpdateUser.kt @@ -20,16 +20,17 @@ import dev.orion.users.enterprise.model.User interface UpdateUser { /** - * Updates user information (email and/or password). + * Updates user information (name, email and/or password). * At least one field must be provided for update. * * @param email : Current user's email + * @param name : New name (optional) * @param newEmail : New email (optional) * @param password : Current password (required if updating password) * @param newPassword : New password (optional) * @return An User object with updated fields * @throws IllegalArgumentException if no fields are provided for update or validation fails */ - fun updateUser(email: String, newEmail: String?, password: String?, newPassword: String?): User + fun updateUser(email: String, name: String?, newEmail: String?, password: String?, newPassword: String?): User } diff --git a/src/main/kotlin/dev/orion/users/application/usecases/UpdateUserImpl.kt b/src/main/kotlin/dev/orion/users/application/usecases/UpdateUserImpl.kt index feaed32..62b7c80 100644 --- a/src/main/kotlin/dev/orion/users/application/usecases/UpdateUserImpl.kt +++ b/src/main/kotlin/dev/orion/users/application/usecases/UpdateUserImpl.kt @@ -29,24 +29,25 @@ class UpdateUserImpl : UpdateUser { private val BLANK = "Blank Arguments" /** - * Updates user information (email and/or password). + * Updates user information (name, email and/or password). * At least one field must be provided for update. * * @param email : Current user's email + * @param name : New name (optional) * @param newEmail : New email (optional) * @param password : Current password (required if updating password) * @param newPassword : New password (optional) * @return An User object with updated fields * @throws IllegalArgumentException if no fields are provided for update or validation fails */ - override fun updateUser(email: String, newEmail: String?, password: String?, newPassword: String?): User { + override fun updateUser(email: String, name: String?, newEmail: String?, password: String?, newPassword: String?): User { if (email.isBlank()) { throw IllegalArgumentException(BLANK) } // Validate that at least one field is being updated - if (newEmail.isNullOrBlank() && newPassword.isNullOrBlank()) { - throw IllegalArgumentException("At least one field (newEmail or newPassword) must be provided for update") + if (name.isNullOrBlank() && newEmail.isNullOrBlank() && newPassword.isNullOrBlank()) { + throw IllegalArgumentException("At least one field (name, newEmail or newPassword) must be provided for update") } // Validate current email format @@ -57,6 +58,14 @@ class UpdateUserImpl : UpdateUser { val user = User() user.email = email + // Update name if provided + if (!name.isNullOrBlank()) { + if (name.trim().isEmpty()) { + throw IllegalArgumentException("Name cannot be empty") + } + user.name = name.trim() + } + // Update email if provided if (!newEmail.isNullOrBlank()) { if (!EmailValidator.getInstance().isValid(newEmail)) { diff --git a/src/main/kotlin/dev/orion/users/frameworks/rest/users/UserWS.kt b/src/main/kotlin/dev/orion/users/frameworks/rest/users/UserWS.kt index 5d2ecab..7427d47 100644 --- a/src/main/kotlin/dev/orion/users/frameworks/rest/users/UserWS.kt +++ b/src/main/kotlin/dev/orion/users/frameworks/rest/users/UserWS.kt @@ -26,10 +26,13 @@ import jakarta.validation.constraints.Email import jakarta.validation.constraints.NotEmpty import jakarta.ws.rs.Consumes import jakarta.ws.rs.FormParam +import jakarta.ws.rs.GET import jakarta.ws.rs.POST import jakarta.ws.rs.PUT import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam import jakarta.ws.rs.Produces +import jakarta.ws.rs.QueryParam import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.core.Response import org.eclipse.microprofile.faulttolerance.Retry @@ -111,10 +114,11 @@ class UserWS { } /** - * Updates user information (email and/or password). Requires authentication via JWT token. - * At least one field (newEmail or newPassword) must be provided. + * Updates user information (name, email and/or password). Requires authentication via JWT token with role "user". + * At least one field (name, newEmail or newPassword) must be provided. * * @param email The current email of the user + * @param name The new name (optional) * @param newEmail The new email address (optional) * @param password The current password (required if updating password) * @param newPassword The new password (optional) @@ -129,6 +133,7 @@ class UserWS { @Retry(maxRetries = 1, delay = 2000) fun updateUser( @RestForm @NotEmpty @Email email: String, + @RestForm name: String?, @RestForm newEmail: String?, @RestForm password: String?, @RestForm newPassword: String? @@ -141,7 +146,34 @@ class UserWS { Response.Status.UNAUTHORIZED ) - return controller.updateUser(email, newEmail, password, newPassword, jwtEmail) + // Extract groups/roles from JWT token + val groups: Set = try { + jwt.getClaim>(Claims.groups.name) + ?: jwt.getClaim>("groups")?.toSet() + ?: emptySet() + } catch (e: Exception) { + emptySet() + } + + // Verifica se é admin (admins também têm role "user") + val isAdmin = groups.contains("admin") + + // Se não for admin, só pode atualizar seu próprio usuário + // Se for admin, pode atualizar qualquer usuário + if (!isAdmin && email != jwtEmail) { + throw ServiceException( + "You can only update your own user", + Response.Status.FORBIDDEN + ) + } + + // Normaliza campos vazios para null + val normalizedName = if (name.isNullOrBlank()) null else name.trim() + val normalizedNewEmail = if (newEmail.isNullOrBlank()) null else newEmail.trim() + val normalizedPassword = if (password.isNullOrBlank()) null else password + val normalizedNewPassword = if (newPassword.isNullOrBlank()) null else newPassword + + return controller.updateUser(email, normalizedName, normalizedNewEmail, normalizedPassword, normalizedNewPassword, jwtEmail, isAdmin) .onItem().transform { response -> Response.ok(response).build() } .onFailure().transform { e -> val message = e.message ?: "Unknown error" @@ -199,5 +231,59 @@ class UserWS { throw ServiceException(message, status) } } + + /** + * Lists all users in the service. Requires admin role. + * + * @return A list of all users in JSON format + * @throws Unauthorized if the user is not an admin + */ + @GET + @Path("/list") + @RolesAllowed("admin") + @Produces(MediaType.APPLICATION_JSON) + @Retry(maxRetries = 1, delay = 2000) + fun listUsers(): Uni { + return controller.listAllUsers() + .onItem().transform { users -> Response.ok(users).build() } + .onFailure().transform { e -> + val message = e.message ?: "Unknown error" + val status = if (message.contains("Unauthorized") || message.contains("token")) { + Response.Status.UNAUTHORIZED + } else { + Response.Status.INTERNAL_SERVER_ERROR + } + throw ServiceException(message, status) + } + } + + /** + * Gets a user by email. Requires admin role. + * + * @param email The email of the user to retrieve + * @return The user object in JSON format + * @throws Bad request if the user is not found + * @throws Unauthorized if the user is not an admin + */ + @GET + @Path("/by-email") + @RolesAllowed("admin") + @Produces(MediaType.APPLICATION_JSON) + @Retry(maxRetries = 1, delay = 2000) + fun getUserByEmail( + @QueryParam("email") @NotEmpty @Email email: String + ): Uni { + return controller.getUserByEmail(email) + .onItem().transform { user -> Response.ok(user).build() } + .onFailure().transform { e -> + val message = e.message ?: "Unknown error" + val status = when { + message.contains("not found") -> Response.Status.NOT_FOUND + message.contains("Unauthorized") || message.contains("token") -> Response.Status.UNAUTHORIZED + else -> Response.Status.BAD_REQUEST + } + throw ServiceException(message, status) + } + } } diff --git a/src/main/resources/META-INF/resources/.gitignore b/src/main/resources/META-INF/resources/.gitignore index b59f7e3..1f1c334 100644 --- a/src/main/resources/META-INF/resources/.gitignore +++ b/src/main/resources/META-INF/resources/.gitignore @@ -1 +1,2 @@ -test/ \ No newline at end of file +test/ +console/ \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/admin/.gitignore b/src/main/resources/META-INF/resources/admin/.gitignore new file mode 100644 index 0000000..ed36915 --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Environment variables +.env +.env.local +.env.*.local + diff --git a/src/main/resources/META-INF/resources/admin/README.md b/src/main/resources/META-INF/resources/admin/README.md new file mode 100644 index 0000000..d2ad03a --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/README.md @@ -0,0 +1,87 @@ +# Admin - Gerenciamento de Usuários + +Aplicação Vue 3 com Vuetify para gerenciamento administrativo de usuários. + +## Estrutura do Projeto + +``` +admin/ +├── src/ +│ ├── components/ # Componentes reutilizáveis +│ │ └── DeleteUserDialog.vue +│ ├── router/ # Configuração de rotas +│ │ └── index.js +│ ├── services/ # Serviços de API +│ │ └── api.js +│ ├── stores/ # Stores Pinia +│ │ └── users.js +│ ├── views/ # Views principais +│ │ ├── LoginView.vue +│ │ ├── UsersListView.vue +│ │ ├── UserDetailView.vue +│ │ ├── CreateUserView.vue +│ │ └── EditUserView.vue +│ ├── App.vue # Componente raiz +│ └── main.js # Entry point +├── index.html +├── package.json +├── vite.config.js +└── README.md +``` + +## Funcionalidades + +- **Autenticação**: Login com verificação de role admin +- **Listagem de Usuários**: Tabela com todos os usuários, filtros e busca +- **Visualização de Detalhes**: Exibição completa das informações do usuário +- **Criação de Usuários**: Formulário para criar novos usuários +- **Edição de Usuários**: Atualização de e-mail e senha +- **Exclusão de Usuários**: Deletar usuários com confirmação + +## Requisitos + +- Node.js 18+ +- npm ou yarn + +## Instalação + +```bash +cd src/main/resources/META-INF/resources/admin +npm install +``` + +## Desenvolvimento + +```bash +npm run dev +``` + +A aplicação estará disponível em `http://localhost:3001/console/` + +## Build + +```bash +npm run build +``` + +Os arquivos serão gerados em `target/classes/META-INF/resources/console/` + +## Endpoints da API Utilizados + +- `GET /users/list` - Lista todos os usuários (admin only) +- `GET /users/by-email?email={email}` - Busca usuário por email (admin only) +- `POST /users/create` - Cria novo usuário +- `PUT /users/update` - Atualiza usuário +- `POST /users/delete` - Deleta usuário (admin only) +- `POST /users/login` - Autenticação + +## Autenticação + +A aplicação requer autenticação com role `admin` no JWT token. O token é armazenado no `localStorage` e incluído automaticamente nas requisições via interceptor do Axios. + +## Notas + +- A aplicação está configurada para ser servida em `/console/` pelo Quarkus +- Em desenvolvimento, o Vite roda na porta 3001 com proxy para a API +- O build gera os arquivos diretamente no diretório de recursos do Quarkus (`target/classes/META-INF/resources/console/`) + diff --git a/src/main/resources/META-INF/resources/admin/index.html b/src/main/resources/META-INF/resources/admin/index.html new file mode 100644 index 0000000..838e934 --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/index.html @@ -0,0 +1,14 @@ + + + + + + + Orion Users + + +
+ + + + diff --git a/src/main/resources/META-INF/resources/admin/package-lock.json b/src/main/resources/META-INF/resources/admin/package-lock.json new file mode 100644 index 0000000..f9e2415 --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/package-lock.json @@ -0,0 +1,1573 @@ +{ + "name": "users-admin", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "users-admin", + "version": "1.0.0", + "dependencies": { + "@mdi/font": "^7.4.47", + "axios": "^1.6.7", + "pinia": "^2.1.7", + "vue": "^3.4.21", + "vue-router": "^4.3.0", + "vuetify": "^3.5.10" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "vite": "^5.1.4", + "vite-plugin-vuetify": "^2.0.2" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@mdi/font": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz", + "integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==", + "license": "Apache-2.0" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.24.tgz", + "integrity": "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.24", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.24.tgz", + "integrity": "sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.24", + "@vue/shared": "3.5.24" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.24.tgz", + "integrity": "sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.24", + "@vue/compiler-dom": "3.5.24", + "@vue/compiler-ssr": "3.5.24", + "@vue/shared": "3.5.24", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.24.tgz", + "integrity": "sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.24", + "@vue/shared": "3.5.24" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz", + "integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.24" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz", + "integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.24", + "@vue/shared": "3.5.24" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz", + "integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.24", + "@vue/runtime-core": "3.5.24", + "@vue/shared": "3.5.24", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz", + "integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.24", + "@vue/shared": "3.5.24" + }, + "peerDependencies": { + "vue": "3.5.24" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.24.tgz", + "integrity": "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==", + "license": "MIT" + }, + "node_modules/@vuetify/loader-shared": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-2.1.1.tgz", + "integrity": "sha512-jSZTzTYaoiv8iwonFCVZQ0YYX/M+Uyl4ng+C4egMJT0Hcmh9gIxJL89qfZICDeo3g0IhqrvipW2FFKKRDMtVcA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "upath": "^2.0.1" + }, + "peerDependencies": { + "vue": "^3.0.0", + "vuetify": "^3.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "devOptional": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/upath": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", + "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vuetify": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.1.2.tgz", + "integrity": "sha512-I/wd6QS+DO6lHmuGoi1UTyvvBTQ2KDzQZ9oowJQEJ6OcjWfJnscYXx2ptm6S7fJSASuZT8jGRBL3LV4oS3LpaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@vuetify/loader-shared": "^2.1.1", + "debug": "^4.3.3", + "upath": "^2.0.1" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": ">=5", + "vue": "^3.0.0", + "vuetify": "^3.0.0" + } + }, + "node_modules/vue": { + "version": "3.5.24", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.24.tgz", + "integrity": "sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.24", + "@vue/compiler-sfc": "3.5.24", + "@vue/runtime-dom": "3.5.24", + "@vue/server-renderer": "3.5.24", + "@vue/shared": "3.5.24" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz", + "integrity": "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vuetify": { + "version": "3.10.11", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.10.11.tgz", + "integrity": "sha512-hfllXT0/C3O5nZyIRalaDU7ClMIrKrKAbjH0T8xbSUb7FcJrHOqPZfEkSXwrKxajv6EA1rwEOvCZoLDhunnjrQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/johnleider" + }, + "peerDependencies": { + "typescript": ">=4.7", + "vite-plugin-vuetify": ">=2.1.0", + "vue": "^3.5.0", + "webpack-plugin-vuetify": ">=3.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vite-plugin-vuetify": { + "optional": true + }, + "webpack-plugin-vuetify": { + "optional": true + } + } + } + } +} diff --git a/src/main/resources/META-INF/resources/admin/package.json b/src/main/resources/META-INF/resources/admin/package.json new file mode 100644 index 0000000..8dc9fb2 --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/package.json @@ -0,0 +1,24 @@ +{ + "name": "users-admin", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.4.21", + "vue-router": "^4.3.0", + "pinia": "^2.1.7", + "axios": "^1.6.7", + "vuetify": "^3.5.10", + "@mdi/font": "^7.4.47" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.4", + "vite": "^5.1.4", + "vite-plugin-vuetify": "^2.0.2" + } +} + diff --git a/src/main/resources/META-INF/resources/admin/src/App.vue b/src/main/resources/META-INF/resources/admin/src/App.vue new file mode 100644 index 0000000..5f134d7 --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/src/App.vue @@ -0,0 +1,129 @@ + + + + + + diff --git a/src/main/resources/META-INF/resources/admin/src/components/DeleteUserDialog.vue b/src/main/resources/META-INF/resources/admin/src/components/DeleteUserDialog.vue new file mode 100644 index 0000000..339ae9d --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/src/components/DeleteUserDialog.vue @@ -0,0 +1,84 @@ + + + + diff --git a/src/main/resources/META-INF/resources/admin/src/main.js b/src/main/resources/META-INF/resources/admin/src/main.js new file mode 100644 index 0000000..0276249 --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/src/main.js @@ -0,0 +1,26 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' +import router from './router' +import 'vuetify/styles' +import { createVuetify } from 'vuetify' +import * as components from 'vuetify/components' +import * as directives from 'vuetify/directives' +import '@mdi/font/css/materialdesignicons.css' + +const vuetify = createVuetify({ + components, + directives, + theme: { + defaultTheme: 'light' + } +}) + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) +app.use(vuetify) + +app.mount('#app') + diff --git a/src/main/resources/META-INF/resources/admin/src/router/index.js b/src/main/resources/META-INF/resources/admin/src/router/index.js new file mode 100644 index 0000000..57d5c31 --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/src/router/index.js @@ -0,0 +1,122 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useUsersStore } from '../stores/users' + +const router = createRouter({ + history: createWebHistory('/console/'), + routes: [ + { + path: '/', + redirect: '/login' + }, + { + path: '/login', + name: 'Login', + component: () => import('../views/LoginView.vue'), + meta: { requiresAuth: false } + }, + { + path: '/users', + name: 'UsersList', + component: () => import('../views/UsersListView.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: '/users/create', + name: 'CreateUser', + component: () => import('../views/CreateUserView.vue'), + meta: { requiresAuth: true, requiresAdmin: true } + }, + { + path: '/users/:email', + name: 'UserDetail', + component: () => import('../views/UserDetailView.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + props: true + }, + { + path: '/users/:email/edit', + name: 'EditUser', + component: () => import('../views/EditUserView.vue'), + meta: { requiresAuth: true, requiresAdmin: true }, + props: true + } + ] +}) + +// Navigation guard to check authentication and admin role +router.beforeEach((to, from, next) => { + const usersStore = useUsersStore() + + // Verifica autenticação diretamente do localStorage + const token = localStorage.getItem('auth_token') + const user = localStorage.getItem('user') + const isAuthenticated = !!(token && user) + + // Se está na rota raiz (/), redireciona baseado na autenticação + if (to.path === '/' || to.path === '') { + if (isAuthenticated) { + next({ name: 'UsersList' }) + return + } else { + next({ name: 'Login' }) + return + } + } + + // Check if route requires authentication + if (to.meta.requiresAuth) { + if (!isAuthenticated) { + // Redirect to login if not authenticated + next({ name: 'Login', query: { redirect: to.fullPath } }) + return + } + + // Check if route requires admin role + if (to.meta.requiresAdmin) { + // Check if user has admin role in JWT token + const token = localStorage.getItem('auth_token') + if (token) { + try { + // Decode JWT token (simple base64 decode, not full validation) + const payload = JSON.parse(atob(token.split('.')[1])) + const groups = payload.groups || [] + + if (!groups.includes('admin')) { + // User is not admin, redirect to login + usersStore.logout() + next({ name: 'Login', query: { error: 'Acesso negado. Apenas administradores podem acessar esta área.' } }) + return + } + } catch (e) { + // Invalid token, logout and redirect to login + usersStore.logout() + next({ name: 'Login', query: { error: 'Token inválido.' } }) + return + } + } else { + next({ name: 'Login', query: { redirect: to.fullPath } }) + return + } + } + } + + // If already authenticated and trying to access login, redirect to users list + // Mas só se realmente estiver autenticado (verifica novamente para evitar loops) + if (to.name === 'Login') { + const token = localStorage.getItem('auth_token') + const user = localStorage.getItem('user') + if (token && user) { + // Usuário ainda está autenticado, redireciona para users list + next({ name: 'UsersList' }) + return + } + // Usuário não está autenticado, permite acesso à tela de login + next() + return + } + + next() +}) + +export default router + diff --git a/src/main/resources/META-INF/resources/admin/src/services/api.js b/src/main/resources/META-INF/resources/admin/src/services/api.js new file mode 100644 index 0000000..abb2834 --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/src/services/api.js @@ -0,0 +1,111 @@ +import axios from 'axios' + +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080' + +const api = axios.create({ + baseURL: API_URL, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } +}) + +// Interceptor to add JWT token to requests +api.interceptors.request.use( + (config) => { + const token = localStorage.getItem('auth_token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => { + return Promise.reject(error) + } +) + +// Interceptor to handle response errors +api.interceptors.response.use( + (response) => { + return response + }, + (error) => { + if (error.response?.status === 401) { + localStorage.removeItem('auth_token') + localStorage.removeItem('user') + // Redirect to login if not already there + if (window.location.pathname !== '/console/login') { + window.location.href = '/console/login' + } + } + return Promise.reject(error) + } +) + +// Helper function to convert object to FormData +const toFormData = (data) => { + const formData = new URLSearchParams() + Object.keys(data).forEach(key => { + const value = data[key] + // Não inclui null, undefined ou strings vazias + if (value !== null && value !== undefined && value !== '') { + formData.append(key, value) + } + }) + return formData +} + +export const userApi = { + // List all users (admin only) + listUsers: () => { + return api.get('/users/list') + }, + + // Get user by email (admin only) + getUserByEmail: (email) => { + return api.get('/users/by-email', { + params: { email } + }) + }, + + // Create user + createUser: (name, email, password) => { + return api.post('/users/create', toFormData({ name, email, password })) + }, + + // Update user + updateUser: (email, name, newEmail, password, newPassword) => { + const data = { email } + // Só inclui campos que têm valores (não null, undefined ou string vazia) + if (name && name.trim() !== '') { + data.name = name.trim() + } + if (newEmail && newEmail.trim() !== '') { + data.newEmail = newEmail.trim() + } + if (password && password !== '') { + data.password = password + } + if (newPassword && newPassword !== '') { + data.newPassword = newPassword + } + + return api.put('/users/update', toFormData(data), { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + }, + + // Delete user (admin only) + deleteUser: (email) => { + return api.post('/users/delete', toFormData({ email })) + }, + + // Login + login: (email, password) => { + return api.post('/users/login', toFormData({ email, password })) + } +} + +export default api + diff --git a/src/main/resources/META-INF/resources/admin/src/stores/users.js b/src/main/resources/META-INF/resources/admin/src/stores/users.js new file mode 100644 index 0000000..d99c83c --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/src/stores/users.js @@ -0,0 +1,204 @@ +import { defineStore } from 'pinia' +import { userApi } from '../services/api' + +export const useUsersStore = defineStore('users', { + state: () => ({ + users: [], + currentUser: null, + loading: false, + error: null, + searchQuery: '', + filterEmailValid: null, // null = all, true = valid, false = invalid + filter2FA: null // null = all, true = enabled, false = disabled + }), + + getters: { + filteredUsers: (state) => { + let filtered = [...state.users] + + // Filter by search query (name or email) + if (state.searchQuery) { + const query = state.searchQuery.toLowerCase() + filtered = filtered.filter(user => + (user.name && user.name.toLowerCase().includes(query)) || + (user.email && user.email.toLowerCase().includes(query)) + ) + } + + // Filter by email validation status + if (state.filterEmailValid !== null) { + filtered = filtered.filter(user => user.emailValid === state.filterEmailValid) + } + + // Filter by 2FA status + if (state.filter2FA !== null) { + filtered = filtered.filter(user => user.using2FA === state.filter2FA) + } + + return filtered + }, + + isAuthenticated: () => { + const token = localStorage.getItem('auth_token') + const user = localStorage.getItem('user') + return !!(token && user) + }, + + currentAdminUser: () => { + const userStr = localStorage.getItem('user') + if (userStr) { + try { + return JSON.parse(userStr) + } catch (e) { + return null + } + } + return null + } + }, + + actions: { + async fetchUsers() { + this.loading = true + this.error = null + try { + const response = await userApi.listUsers() + this.users = response.data || [] + } catch (error) { + this.error = error.response?.data?.message || error.message || 'Erro ao carregar usuários' + throw error + } finally { + this.loading = false + } + }, + + async fetchUserByEmail(email) { + this.loading = true + this.error = null + try { + const response = await userApi.getUserByEmail(email) + this.currentUser = response.data + return response.data + } catch (error) { + this.error = error.response?.data?.message || error.message || 'Erro ao carregar usuário' + throw error + } finally { + this.loading = false + } + }, + + async createUser(userData) { + this.loading = true + this.error = null + try { + const response = await userApi.createUser( + userData.name, + userData.email, + userData.password + ) + // Refresh users list + await this.fetchUsers() + return response.data + } catch (error) { + this.error = error.response?.data?.message || error.message || 'Erro ao criar usuário' + throw error + } finally { + this.loading = false + } + }, + + async updateUser(userData) { + this.loading = true + this.error = null + try { + // Normaliza campos: converte strings vazias para null/undefined + const name = userData.name && userData.name.trim() !== '' ? userData.name.trim() : null + const newEmail = userData.newEmail && userData.newEmail.trim() !== '' ? userData.newEmail.trim() : null + const password = userData.password && userData.password !== '' ? userData.password : null + const newPassword = userData.newPassword && userData.newPassword !== '' ? userData.newPassword : null + + const response = await userApi.updateUser( + userData.email, + name, + newEmail, + password, + newPassword + ) + // Refresh users list + await this.fetchUsers() + return response.data + } catch (error) { + this.error = error.response?.data?.message || error.message || 'Erro ao atualizar usuário' + throw error + } finally { + this.loading = false + } + }, + + async deleteUser(email) { + this.loading = true + this.error = null + try { + await userApi.deleteUser(email) + // Refresh users list + await this.fetchUsers() + } catch (error) { + this.error = error.response?.data?.message || error.message || 'Erro ao deletar usuário' + throw error + } finally { + this.loading = false + } + }, + + async login(email, password) { + this.loading = true + this.error = null + try { + const response = await userApi.login(email, password) + + if (response.data?.authentication) { + const authData = response.data.authentication + localStorage.setItem('auth_token', authData.token) + localStorage.setItem('user', JSON.stringify(authData.user)) + return authData + } else if (response.data?.requires2FA) { + // Handle 2FA requirement + throw new Error('2FA é necessário para este usuário') + } else { + throw new Error('Resposta de autenticação inválida') + } + } catch (error) { + this.error = error.response?.data?.message || error.message || 'Erro ao fazer login' + throw error + } finally { + this.loading = false + } + }, + + logout() { + localStorage.removeItem('auth_token') + localStorage.removeItem('user') + this.currentUser = null + this.users = [] + }, + + setSearchQuery(query) { + this.searchQuery = query + }, + + setFilterEmailValid(value) { + this.filterEmailValid = value + }, + + setFilter2FA(value) { + this.filter2FA = value + }, + + clearFilters() { + this.searchQuery = '' + this.filterEmailValid = null + this.filter2FA = null + } + } +}) + diff --git a/src/main/resources/META-INF/resources/admin/src/views/CreateUserView.vue b/src/main/resources/META-INF/resources/admin/src/views/CreateUserView.vue new file mode 100644 index 0000000..01a3f95 --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/src/views/CreateUserView.vue @@ -0,0 +1,194 @@ + + + + diff --git a/src/main/resources/META-INF/resources/admin/src/views/EditUserView.vue b/src/main/resources/META-INF/resources/admin/src/views/EditUserView.vue new file mode 100644 index 0000000..e8b68dd --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/src/views/EditUserView.vue @@ -0,0 +1,262 @@ + + + + diff --git a/src/main/resources/META-INF/resources/admin/src/views/LoginView.vue b/src/main/resources/META-INF/resources/admin/src/views/LoginView.vue new file mode 100644 index 0000000..a1f76b0 --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/src/views/LoginView.vue @@ -0,0 +1,160 @@ + + + + diff --git a/src/main/resources/META-INF/resources/admin/src/views/UserDetailView.vue b/src/main/resources/META-INF/resources/admin/src/views/UserDetailView.vue new file mode 100644 index 0000000..2b3ff49 --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/src/views/UserDetailView.vue @@ -0,0 +1,281 @@ + + + + + + diff --git a/src/main/resources/META-INF/resources/admin/src/views/UsersListView.vue b/src/main/resources/META-INF/resources/admin/src/views/UsersListView.vue new file mode 100644 index 0000000..7e67c36 --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/src/views/UsersListView.vue @@ -0,0 +1,241 @@ + + + + diff --git a/src/main/resources/META-INF/resources/admin/vite.config.js b/src/main/resources/META-INF/resources/admin/vite.config.js new file mode 100644 index 0000000..e3dd67c --- /dev/null +++ b/src/main/resources/META-INF/resources/admin/vite.config.js @@ -0,0 +1,62 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vuetify from 'vite-plugin-vuetify' +import { fileURLToPath, URL } from 'node:url' + +export default defineConfig({ + // Configurar base para servir em /console/ tanto em desenvolvimento quanto em produção + base: '/console/', + plugins: [ + vue(), + vuetify({ autoImport: true }) + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + build: { + // Gerar arquivos diretamente em META-INF/resources/console/ + // para serem servidos pelo Quarkus em http://localhost:8080/console/ + outDir: '../console', + emptyOutDir: false, // Não esvaziar o diretório pois está fora do projeto root + rollupOptions: { + output: { + manualChunks(id) { + // Separar bibliotecas grandes em chunks próprios + if (id.includes('node_modules')) { + // Vuetify é grande e pode ser separado + if (id.includes('vuetify')) { + return 'vuetify' + } + // Vue, Vue Router e Pinia são necessários desde o início + if (id.includes('vue') || id.includes('vue-router') || id.includes('pinia')) { + return 'vue-vendor' + } + // Outras dependências + if (id.includes('axios')) { + return 'vendor' + } + } + } + } + }, + // Desabilitar preload automático de módulos para evitar avisos + modulePreload: { + polyfill: false, + resolveDependencies: () => [] + }, + chunkSizeWarningLimit: 1000 + }, + server: { + // Em desenvolvimento, Vite roda em porta diferente do Quarkus + port: 3001, + proxy: { + '/users': { + target: 'http://localhost:8080', + changeOrigin: true + } + } + } +}) + diff --git a/src/main/resources/META-INF/resources/playground/src/views/LoginView.vue b/src/main/resources/META-INF/resources/playground/src/views/LoginView.vue index 01cbb10..59e3f54 100644 --- a/src/main/resources/META-INF/resources/playground/src/views/LoginView.vue +++ b/src/main/resources/META-INF/resources/playground/src/views/LoginView.vue @@ -252,10 +252,18 @@ const handleRegister = async () => { ) const data = response.data - if (data.token && data.user) { + // Backend returns LoginResponseDTO with authentication object + if (data.authentication) { + authStore.setAuth(data.authentication.token, data.authentication.user) + showMessage('Registration successful!') + router.push('/dashboard') + } else if (data.token && data.user) { + // Fallback for old format (should not happen with current backend) authStore.setAuth(data.token, data.user) showMessage('Registration successful!') router.push('/dashboard') + } else { + showMessage('Unexpected response format', 'error') } } catch (error) { const message = error.response?.data?.message || error.message || 'Error registering user' diff --git a/src/main/resources/import.sql b/src/main/resources/import.sql index e73dd92..d2afc65 100644 --- a/src/main/resources/import.sql +++ b/src/main/resources/import.sql @@ -1,2 +1,12 @@ INSERT INTO Role (id, name) VALUES (1, 'admin'); -INSERT INTO Role (id, name) VALUES (2, 'user'); \ No newline at end of file +INSERT INTO Role (id, name) VALUES (2, 'user'); + +-- Admin user: admin@orion.dev / orionadmin +-- Password hash (SHA256): 24febcc27e4a5762911a4481a941a3563cc4bf5e5f61f0ea3799333871d2a89b +INSERT INTO User (id, hash, name, email, password, emailValid, emailValidationCode, isUsing2FA, secret2FA, require2FAForBasicLogin, require2FAForSocialLogin) +VALUES (1, '00000000-0000-0000-0000-000000000001', 'Administrator', 'admin@orion.dev', '24febcc27e4a5762911a4481a941a3563cc4bf5e5f61f0ea3799333871d2a89b', true, '00000000-0000-0000-0000-000000000001', false, NULL, false, false); + +-- Associar roles ao usuário admin (admin e user) +-- A tabela de junção está definida explicitamente na entidade UserEntity como "User_Role" +INSERT INTO User_Role (User_id, roles_id) VALUES (1, 1); -- admin +INSERT INTO User_Role (User_id, roles_id) VALUES (1, 2); -- user