From da2cb493c9da633b14f2881fad956a276807ba16 Mon Sep 17 00:00:00 2001 From: Rodrigo Prestes Machado Date: Sun, 23 Nov 2025 21:32:49 -0300 Subject: [PATCH] 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