Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4722b44
BE lastActivity field added to the UserAccount entity
Sep 8, 2025
f35eb83
Update DEVELOPMENT.md
Sep 8, 2025
260a148
FE Add last activity to AdministrationUsers.tsx
Sep 8, 2025
b9b1fd2
FE Regenerate API schemas
Sep 8, 2025
e98ed1c
BE adjust the implementation to work with UserAccountView
Sep 8, 2025
f65ece4
chore: Unify last activity UI to existing code & fix UI issues
Oct 20, 2025
6a98421
style: correct spelling of strings
Oct 21, 2025
9fa7a29
style: format & unify DEVELOPMENT.md
Oct 21, 2025
1b153d8
doc: add running backend dependency for schema generation to DEVELOPM…
Oct 21, 2025
adbc0a1
chore: rename files in separate commit to keep their history
Oct 22, 2025
9881594
feat: implement last activity via new `UserAccountAdministrationView`…
Oct 22, 2025
84e9fbf
style: use `{theme.palette.text.secondary}` instead of `'textSecondary'`
Oct 22, 2025
bc4f01f
fix: reflect forgotten API schema changes
Oct 22, 2025
ff5f0a5
fix: search only within list items (i.e. not in menu)
Oct 23, 2025
f21a136
test: E2E for last activity in users administration
Oct 23, 2025
c0a1c6e
fix: set user activity time in local timezone
Oct 24, 2025
af6393f
test: integration test for last activity in users administration
Oct 24, 2025
9670118
fix: make sure forced date is always released
Oct 24, 2025
731eb8a
fix: make sure forced date is always released even in integration test
Oct 24, 2025
eaff814
Merge branch 'tolgee:main' into user-last-activity
zomp Oct 28, 2025
055045a
fix: use proper server response types
Nov 1, 2025
cab9feb
refactor: unify `currentDateProvider.forcedDate` clearance
Nov 1, 2025
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
77 changes: 49 additions & 28 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
## Install Prerequisites

* [Java 21](https://openjdk.org/install)
* [Docker](https://docs.docker.com/engine/install)
* [Node.js 18](https://nodejs.org/en/download) (or higher)
* [Intellij Idea](https://www.jetbrains.com/help/idea/installation-guide.html) (optional)
- [Java 21](https://openjdk.org/install)
- [Docker](https://docs.docker.com/engine/install)
- [Node.js 18](https://nodejs.org/en/download) (or higher)
- [Intellij Idea](https://www.jetbrains.com/help/idea/installation-guide.html) (optional)

## Clone this repo

Expand All @@ -14,14 +14,14 @@ git clone --depth 1 [email protected]:tolgee/tolgee-platform.git
## Run the stack

1. Run backend
* With the prepared Idea run configuration `Backend localhost`
* With command line:
- With the prepared Idea run configuration `Backend localhost`.
- With command line:
```shell
./gradlew server-app:bootRun --args='--spring.profiles.active=dev'
```
2. Run frontend
* With the prepared Idea run configuration `Frontend localhost`
* With command line:
- With the prepared Idea run configuration `Frontend localhost`.
- With command line:
```shell
cd webapp && npm ci && npm run start
```
Expand All @@ -33,7 +33,7 @@ The backend of Tolgee is tested with unit and integration tests.

### Backend testing

To run backend tests, you can run Gradle test task
To run backend tests, you can run Gradle test task:

```shell
./gradlew test
Expand Down Expand Up @@ -62,11 +62,29 @@ tolgee:
file-storage-url: http://localhost:8080
```

To enable authentication, add following properties:

```yaml
tolgee:
authentication:
enabled: true
initial-username: <YOUR_REAL_EMAIL>
initial-password: admin
```

You can check `application-e2e.yaml` for further inspiration.
To learn more about externalized configuration in Spring boot, read [the docs](https://docs.spring.io/spring-boot/3.4/reference/features/external-config.html).
To learn more about externalized configuration in Spring Boot, read [its docs](https://docs.spring.io/spring-boot/3.4/reference/features/external-config.html).

Since we set the active profile to `dev`, Spring uses the `application-dev.yaml` configuration file.

## API schema changes

After you change the API schema, you need to have backend running and invoke the webapp schema script to update frontend:

```shell
cd webapp && npm run schema
```

## Updating the database changelog

Tolgee uses Liquibase to handle the database migration. The migrations are run on every app startup. To update the changelog, run:
Expand All @@ -77,12 +95,13 @@ Tolgee uses Liquibase to handle the database migration. The migrations are run o

### Troubleshooting updating the changelog

If you misspell the command and run diffChangelog, it will find the command, but it would fail, since liquibase changed the command name in the past.
We have enhanced the diffChangeLog (with capital L) command, so you have to run that.
If you misspell the command and run `diffChangelog`, it will find the command, but it would fail, since Liquibase changed the command name in the past.
We have enhanced the `diffChangeLog` (with capital `L`) command, so you have to run that.

Sometimes, Gradle cannot find a docker command to start the database instance to generate the changelog against.
This happens due to some issue with setting the paths for Gradle daemon.
Sometimes, Gradle cannot find a Docker command to start the database instance to generate the changelog against.
This happens due to some issue with setting the paths for Gradle Daemon.
Running the command without daemon fixes the issue:

```shell
./gradlew diffChangeLog --no-daemon
```
Expand All @@ -91,9 +110,9 @@ Running the command without daemon fixes the issue:

For the frontend, there are npm tasks `prettier` and `eslint`, which you should run before every commit.
Otherwise, the "Frontend static check" workflow will fail.
You can also use prettier plugins for VS Code, Idea, or WebStorm.
You can also use Prettier plugins for VS Code, Idea, or WebStorm.

To fix prettier issues and check everything is fine, run these commands:
To fix Prettier issues and check everything is fine, run these commands:

```shell
cd webapp
Expand All @@ -102,7 +121,7 @@ npm run tsc
npm run eslint
```

On the backend, there is Gradle task `ktlintFormat`, which helps you to format Kotlin code.
On the backend, there is Gradle task `ktlintFormat`, which helps you to format Kotlin code:

```shell
./gradlew ktlintFormat
Expand All @@ -122,19 +141,20 @@ VITE_APP_TOLGEE_API_KEY=your-tolgee-api-key

## Troubleshooting

### Command not found when executing gradle tasks on MacOS
When running E2e Tests from Idea on Mac, you encounter fails due to command not found.
### Command not found when executing Gradle tasks on MacOS

Apparentrly this happens because IDEA starts the gradle daemon with wrong path.
When running E2E tests from Idea on Mac, you encounter fails due to command not found.

The only **workaround** I currently found is killing the gradle daemon and running IDEA from terminal
Apparently this happens because Idea starts the Gradle Daemon with wrong path.

```bash
The only **workaround** I currently found is killing the Gradle Daemon and running Idea from terminal:

```shell
pkill -f '.*GradleDaemon.*'
open -a 'IntelliJ IDEA Ultimate'
```

This way, IDEA is started with correct environment from zsh or bash and so the Gradle Daemon is started correctly.
This way, Idea is started with correct environment from Zsh or Bash and so the Gradle Daemon is started correctly.

If you don't like this solution (I don't like it too), you can start looking for better solution.
This thread is a good starting point: https://discuss.gradle.org/t/exec-execute-in-gradle-doesnt-use-path/25598/3
Expand All @@ -147,7 +167,8 @@ To monitor business activities in the Tolgee platform, we use PostHog for event

When an activity is stored with a modifying endpoint on the backend, the event is automatically logged. Developers can optionally provide additional metadata using the `businessEventData` property in `ActivityHolder`.

Usually, you don't need to provide the data, but If you really need to, you can do it this way.
Usually, you don't need to provide the data, but If you really need to, you can do it this way:

```kotlin
// Example: Adding business event data to an activity
@Component
Expand Down Expand Up @@ -203,10 +224,10 @@ class YourService(

### 3. Logging from frontend code

For logging events from the frontend, use the provided React hooks:
For logging events from the frontend, use the provided React Hooks:

```typescript
// Example 1: Using useReportEvent hook for event-triggered reporting
// Example 1: Using useReportEvent Hook for event-triggered reporting
import { useReportEvent } from 'tg.hooks/useReportEvent';

function ExampleComponent() {
Expand All @@ -222,7 +243,7 @@ function ExampleComponent() {
```

```typescript
// Example 2: Using useReportOnce hook for reporting on component mount
// Example 2: Using useReportOnce Hook for reporting on component mount
import { useReportOnce } from 'tg.hooks/useReportEvent';

function AnotherComponent() {
Expand All @@ -233,4 +254,4 @@ function AnotherComponent() {
}
```

These frontend hooks send events through the backend API, ensuring they aren't blocked by ad blockers.
These frontend Hooks send events through the backend API, ensuring they aren't blocked by ad blockers.
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.api.v2.controllers.IController
import io.tolgee.constants.Message
import io.tolgee.dtos.cacheable.isAdmin
import io.tolgee.dtos.queryResults.UserAccountAdministrationView
import io.tolgee.dtos.queryResults.organization.OrganizationView
import io.tolgee.exceptions.BadRequestException
import io.tolgee.hateoas.organization.OrganizationModel
import io.tolgee.hateoas.organization.OrganizationModelAssembler
import io.tolgee.hateoas.userAccount.UserAccountModel
import io.tolgee.hateoas.userAccount.UserAccountModelAssembler
import io.tolgee.hateoas.userAccount.UserAccountAdministrationModel
import io.tolgee.hateoas.userAccount.UserAccountAdministrationModelAssembler
import io.tolgee.model.UserAccount
import io.tolgee.openApiDocs.OpenApiSelfHostedExtension
import io.tolgee.security.authentication.AuthenticationFacade
Expand Down Expand Up @@ -43,18 +44,18 @@ import org.springframework.web.bind.annotation.RestController
name = "Server Administration",
description =
"**Only for self-hosted instances** \n\n" +
"Managees global Tolgee Platform instance data e.g., user accounts and organizations.",
"Manages global Tolgee Platform instance data, e.g. user accounts and organizations.",
)
@OpenApiSelfHostedExtension
class AdministrationController(
private val organizationService: OrganizationService,
private val pagedOrganizationResourcesAssembler: PagedResourcesAssembler<OrganizationView>,
private val organizationModelAssembler: OrganizationModelAssembler,
private val authenticationFacade: AuthenticationFacade,
private val userAccountService: UserAccountService,
private val pagedResourcesAssembler: PagedResourcesAssembler<UserAccount>,
private val userAccountModelAssembler: UserAccountModelAssembler,
private val jwtService: JwtService,
private val organizationService: OrganizationService,
private val pagedOrganizationResourcesAssembler: PagedResourcesAssembler<OrganizationView>,
private val organizationModelAssembler: OrganizationModelAssembler,
private val authenticationFacade: AuthenticationFacade,
private val userAccountService: UserAccountService,
private val pagedUserResourcesAssembler: PagedResourcesAssembler<UserAccountAdministrationView>,
private val userModelAssembler: UserAccountAdministrationModelAssembler,
private val jwtService: JwtService,
) : IController {
@GetMapping(value = ["/organizations"])
@Operation(summary = "Get all server organizations")
Expand Down Expand Up @@ -82,9 +83,9 @@ class AdministrationController(
@SortDefault(sort = ["name"])
pageable: Pageable,
search: String? = null,
): PagedModel<UserAccountModel> {
): PagedModel<UserAccountAdministrationModel> {
val users = userAccountService.findAllWithDisabledPaged(pageable, search)
return pagedResourcesAssembler.toModel(users, userAccountModelAssembler)
return pagedUserResourcesAssembler.toModel(users, userModelAssembler)
}

@DeleteMapping(value = ["/users/{userId}"])
Expand Down Expand Up @@ -140,7 +141,7 @@ class AdministrationController(

@GetMapping(value = ["/users/{userId:[0-9]+}/generate-token"])
@Operation(
summary = "Geneate user's JWT token",
summary = "Generate user's JWT token",
description =
"Generates a JWT token for the user with provided ID. This is useful, when need to debug of the " +
"user's account. Or when an operation is required to be executed on behalf of the user.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import io.tolgee.dtos.Avatar
import io.tolgee.model.UserAccount
import org.springframework.hateoas.RepresentationModel
import org.springframework.hateoas.server.core.Relation
import java.util.Date

@Relation(collectionRelation = "users", itemRelation = "user")
data class UserAccountModel(
data class UserAccountAdministrationModel(
val id: Long,
val username: String,
val name: String?,
Expand All @@ -16,4 +17,5 @@ data class UserAccountModel(
val mfaEnabled: Boolean,
val deleted: Boolean,
val disabled: Boolean,
) : RepresentationModel<UserAccountModel>()
val lastActivity: Date?,
) : RepresentationModel<UserAccountAdministrationModel>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.tolgee.hateoas.userAccount

import io.tolgee.api.isMfaEnabled
import io.tolgee.api.v2.controllers.V2UserController
import io.tolgee.dtos.queryResults.UserAccountAdministrationView
import io.tolgee.model.UserAccount
import io.tolgee.service.AvatarService
import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport
import org.springframework.stereotype.Component

@Component
class UserAccountAdministrationModelAssembler(
private val avatarService: AvatarService,
) : RepresentationModelAssemblerSupport<UserAccountAdministrationView, UserAccountAdministrationModel>(
V2UserController::class.java,
UserAccountAdministrationModel::class.java,
) {
override fun toModel(view: UserAccountAdministrationView): UserAccountAdministrationModel {
val avatar = avatarService.getAvatarLinks(view.avatarHash)

return UserAccountAdministrationModel(
id = view.id,
username = view.username,
name = view.name,
emailAwaitingVerification = view.emailAwaitingVerification,
avatar = avatar,
globalServerRole = view.role ?: UserAccount.Role.USER,
mfaEnabled = view.isMfaEnabled,
deleted = view.deletedAt != null,
disabled = view.disabledAt != null,
lastActivity = view.lastActivity,
)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package io.tolgee.api.v2.controllers.administration

import io.tolgee.development.testDataBuilder.data.AdministrationTestData
import io.tolgee.dtos.request.LanguageRequest
import io.tolgee.dtos.request.organization.OrganizationDto
import io.tolgee.dtos.request.project.CreateProjectRequest
import io.tolgee.fixtures.andAssertThatJson
import io.tolgee.fixtures.andGetContentAsString
import io.tolgee.fixtures.andIsCreated
Expand All @@ -17,6 +19,7 @@ import org.junit.jupiter.api.Test
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.HttpHeaders
import java.util.Date

@SpringBootTest
@AutoConfigureMockMvc
Expand Down Expand Up @@ -69,6 +72,41 @@ class AdministrationControllerTest : AuthorizedControllerTest() {
}
}

@Test
fun `returns last activity`() {
val prevForcedDate = currentDateProvider.forcedDate
val forcedDate = Date()
currentDateProvider.forcedDate = forcedDate

performAuthGet("/v2/administration/users?search=${userAccount!!.username}").andAssertThatJson {
node("_embedded.users") {
node("[0]") {
node("lastActivity").isNull()
}
}
}

val organization = dbPopulator.createOrganization("just.to.record.activity", userAccount!!)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace dbPopulator with test data from AdministrationTestData.

As per previous feedback, dbPopulator should be phased out in favor of using test data. Use the default organization from AdministrationTestData instead of creating a new one.

For example:

-val organization = dbPopulator.createOrganization("just.to.record.activity", userAccount!!)
+val organization = testData.adminBuilder.defaultOrganizationBuilder.self

Based on past review comments.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
backend/app/src/test/kotlin/io/tolgee/api/v2/controllers/administration/AdministrationControllerTest.kt
around line 94: the test currently creates an organization via dbPopulator which
should be replaced by using the default test organization from
AdministrationTestData; remove the dbPopulator.createOrganization(...) call and
instead obtain the organization from the AdministrationTestData instance (e.g.,
administrationTestData.defaultOrganization or
administrationTestData.organization) that the test setup provides, and ensure
the test uses the corresponding test user from AdministrationTestData where
needed so no new org is created inline.

loginAsUser(userAccount!!.username)
val projectRequest = CreateProjectRequest(
"just.to.record.activity",
listOf(LanguageRequest("cs", "čj", "cs")),
organizationId = organization.id,
icuPlaceholders = true
)
performAuthPost("/v2/projects", projectRequest)

performAuthGet("/v2/administration/users?search=${userAccount!!.username}").andAssertThatJson {
node("_embedded.users") {
node("[0]") {
node("lastActivity").isEqualTo(forcedDate.time)
}
}
}

currentDateProvider.forcedDate = prevForcedDate
}

@Test
fun `sets role`() {
performAuthPut("/v2/administration/users/${testData.user.id}/set-role/ADMIN", null).andIsOk
Expand Down
Loading