Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
77 changes: 64 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -87,40 +105,73 @@ 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
curl -X PUT 'http://localhost:8080/users/update' \
--header 'Authorization: Bearer <token>' \
--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'
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<LoginResponseDTO> {
// 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
Expand All @@ -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!!)
Expand All @@ -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 ->
Expand Down Expand Up @@ -765,5 +791,26 @@ class UserController : BasicController() {
}
}

/**
* Lists all users in the service.
*
* @return A Uni<List<UserEntity>> containing all users
*/
fun listAllUsers(): Uni<List<UserEntity>> {
return userRepository.listAllUsers()
}

/**
* Gets a user by email.
*
* @param email The email of the user
* @return A Uni<UserEntity> containing the user if found
*/
fun getUserByEmail(email: String): Uni<UserEntity> {
return userRepository.findUserByEmail(email)
.onItem().ifNull()
.failWith(IllegalArgumentException("User not found"))
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<RoleEntity> = mutableListOf()

/** Stores if the e-mail was validated. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,12 @@ interface UserRepository : PanacheRepository<UserEntity> {
* @return Returns a Long 1 if user was deleted
*/
fun deleteUser(email: String): Uni<Void>

/**
* Lists all users in the service.
*
* @return A Uni<List<UserEntity>> containing all users
*/
fun listAllUsers(): Uni<List<UserEntity>>
}

Original file line number Diff line number Diff line change
Expand Up @@ -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<List<UserEntity>> containing all users
*/
override fun listAllUsers(): Uni<List<UserEntity>> {
return (this as io.quarkus.hibernate.reactive.panache.PanacheRepository<UserEntity>)
.listAll()
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)) {
Expand Down
Loading
Loading