diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 386ce352a7..7a21a12210 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -62,11 +62,29 @@ tolgee: file-storage-url: http://localhost:8080 ``` +To enable authentication, add following properties: + +```yaml +tolgee: + authentication: + enabled: true + initial-username: + 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). 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 run the webapp schema script to update Frontend: + +```bash +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: diff --git a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/AdministrationController.kt b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/AdministrationController.kt index 150b328368..54c77d399f 100644 --- a/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/AdministrationController.kt +++ b/backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/AdministrationController.kt @@ -3,6 +3,7 @@ package io.tolgee.api.v2.controllers import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import io.tolgee.constants.Message +import io.tolgee.dtos.queryResults.UserAccountView import io.tolgee.dtos.queryResults.organization.OrganizationView import io.tolgee.exceptions.BadRequestException import io.tolgee.hateoas.organization.OrganizationModel @@ -50,7 +51,7 @@ class AdministrationController( private val organizationModelAssembler: OrganizationModelAssembler, private val authenticationFacade: AuthenticationFacade, private val userAccountService: UserAccountService, - private val pagedResourcesAssembler: PagedResourcesAssembler, + private val pagedResourcesAssembler: PagedResourcesAssembler, private val userAccountModelAssembler: UserAccountModelAssembler, private val jwtService: JwtService, ) : IController { diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountModel.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountModel.kt index 079e5166b4..de0a04b4e2 100644 --- a/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountModel.kt +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountModel.kt @@ -4,6 +4,7 @@ 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( @@ -14,6 +15,7 @@ data class UserAccountModel( val avatar: Avatar?, val globalServerRole: UserAccount.Role, val mfaEnabled: Boolean, + val lastActivity: Date?, val deleted: Boolean, val disabled: Boolean, ) : RepresentationModel() diff --git a/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountModelAssembler.kt b/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountModelAssembler.kt index 2bfe9d9df0..20fa94766f 100644 --- a/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountModelAssembler.kt +++ b/backend/api/src/main/kotlin/io/tolgee/hateoas/userAccount/UserAccountModelAssembler.kt @@ -2,6 +2,7 @@ package io.tolgee.hateoas.userAccount import io.tolgee.api.isMfaEnabled import io.tolgee.api.v2.controllers.V2UserController +import io.tolgee.dtos.queryResults.UserAccountView import io.tolgee.model.UserAccount import io.tolgee.service.AvatarService import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport @@ -10,23 +11,24 @@ import org.springframework.stereotype.Component @Component class UserAccountModelAssembler( private val avatarService: AvatarService, -) : RepresentationModelAssemblerSupport( +) : RepresentationModelAssemblerSupport( V2UserController::class.java, UserAccountModel::class.java, ) { - override fun toModel(entity: UserAccount): UserAccountModel { - val avatar = avatarService.getAvatarLinks(entity.avatarHash) + override fun toModel(view: UserAccountView): UserAccountModel { + val avatar = avatarService.getAvatarLinks(view.avatarHash) return UserAccountModel( - id = entity.id, - username = entity.username, - name = entity.name, - emailAwaitingVerification = entity.emailVerification?.newEmail, + id = view.id, + username = view.username, + name = view.name, + emailAwaitingVerification = view.emailAwaitingVerification, avatar = avatar, - globalServerRole = entity.role ?: UserAccount.Role.USER, - mfaEnabled = entity.isMfaEnabled, - deleted = entity.deletedAt != null, - disabled = entity.disabledAt != null, + globalServerRole = view.role ?: UserAccount.Role.USER, + mfaEnabled = view.isMfaEnabled, + deleted = view.deletedAt != null, + disabled = view.disabledAt != null, + lastActivity = view.lastActivity, ) } } diff --git a/backend/data/src/main/kotlin/io/tolgee/dtos/queryResults/UserAccountView.kt b/backend/data/src/main/kotlin/io/tolgee/dtos/queryResults/UserAccountView.kt index 5fe18b5a9a..ad6a0fddda 100644 --- a/backend/data/src/main/kotlin/io/tolgee/dtos/queryResults/UserAccountView.kt +++ b/backend/data/src/main/kotlin/io/tolgee/dtos/queryResults/UserAccountView.kt @@ -3,6 +3,7 @@ package io.tolgee.dtos.queryResults import io.tolgee.api.IUserAccount import io.tolgee.model.UserAccount import io.tolgee.model.enums.ThirdPartyAuthType +import java.util.* class UserAccountView( val id: Long, @@ -15,9 +16,12 @@ class UserAccountView( val role: UserAccount.Role?, override var isInitialUser: Boolean, override val totpKey: ByteArray?, + val deletedAt: Date?, + val disabledAt: Date?, + val lastActivity: Date? ) : IUserAccount { companion object { - fun fromEntity(entity: UserAccount): UserAccountView { + fun fromEntity(entity: UserAccount, lastActivity: Date?): UserAccountView { return UserAccountView( id = entity.id, username = entity.username, @@ -29,6 +33,9 @@ class UserAccountView( role = entity.role ?: UserAccount.Role.USER, isInitialUser = entity.isInitialUser, totpKey = entity.totpKey, + deletedAt = entity.deletedAt, + disabledAt = entity.disabledAt, + lastActivity = lastActivity, ) } } diff --git a/backend/data/src/main/kotlin/io/tolgee/repository/UserAccountRepository.kt b/backend/data/src/main/kotlin/io/tolgee/repository/UserAccountRepository.kt index 723efacb7a..6a574d0314 100644 --- a/backend/data/src/main/kotlin/io/tolgee/repository/UserAccountRepository.kt +++ b/backend/data/src/main/kotlin/io/tolgee/repository/UserAccountRepository.kt @@ -146,7 +146,10 @@ interface UserAccountRepository : JpaRepository { ua.thirdPartyAuthType, ua.role, ua.isInitialUser, - ua.totpKey + ua.totpKey, + ua.deletedAt, + ua.disabledAt, + null ) from UserAccount ua left join ua.emailVerification ev where ua.id = :userAccountId and ua.deletedAt is null and ua.disabledAt is null @@ -258,7 +261,29 @@ interface UserAccountRepository : JpaRepository { @Query( """ + with lastActivityCTE as ( + select ar.authorId as authorId, max(ar.timestamp) as lastActivity + from ActivityRevision ar + group by ar.authorId + ) + select new io.tolgee.dtos.queryResults.UserAccountView( + userAccount.id, + userAccount.username, + userAccount.name, + case when ev is not null then coalesce(ev.newEmail, userAccount.username) else null end, + userAccount.avatarHash, + userAccount.accountType, + userAccount.thirdPartyAuthType, + userAccount.role, + userAccount.isInitialUser, + userAccount.totpKey, + userAccount.deletedAt, + userAccount.disabledAt, + la.lastActivity + ) from UserAccount userAccount + left join userAccount.emailVerification ev + left join lastActivityCTE la on la.authorId = userAccount.id where ((lower(userAccount.name) like lower(concat('%', cast(:search as text),'%')) or lower(userAccount.username) like lower(concat('%', cast(:search as text),'%'))) or cast(:search as text) is null) @@ -268,7 +293,7 @@ interface UserAccountRepository : JpaRepository { fun findAllWithDisabledPaged( search: String?, pageable: Pageable, - ): Page + ): Page @Query( value = """ diff --git a/backend/data/src/main/kotlin/io/tolgee/service/security/UserAccountService.kt b/backend/data/src/main/kotlin/io/tolgee/service/security/UserAccountService.kt index f0b8feab97..5b244d49bb 100644 --- a/backend/data/src/main/kotlin/io/tolgee/service/security/UserAccountService.kt +++ b/backend/data/src/main/kotlin/io/tolgee/service/security/UserAccountService.kt @@ -569,7 +569,7 @@ class UserAccountService( fun findAllWithDisabledPaged( pageable: Pageable, search: String?, - ): Page { + ): Page { return userAccountRepository.findAllWithDisabledPaged(search, pageable) } diff --git a/webapp/src/service/apiSchema.generated.ts b/webapp/src/service/apiSchema.generated.ts index 333c703ab5..798d4aad52 100644 --- a/webapp/src/service/apiSchema.generated.ts +++ b/webapp/src/service/apiSchema.generated.ts @@ -936,9 +936,6 @@ export interface paths { /** Returns initial data required by the UI to load */ get: operations["get_17"]; }; - "/v2/public/llm/prompt": { - post: operations["prompt"]; - }; "/v2/public/machine-translation-providers": { /** Get machine translation providers */ get: operations["getInfo_4"]; @@ -3406,16 +3403,6 @@ export interface components { /** Format: int64 */ untranslatedWordCount: number; }; - LlmMessage: { - image?: string; - text?: string; - type: "TEXT" | "IMAGE"; - }; - LlmParams: { - messages: components["schemas"]["LlmMessage"][]; - priority: "LOW" | "HIGH"; - shouldOutputJson: boolean; - }; LlmProviderModel: { apiKey?: string; apiUrl?: string; @@ -4478,13 +4465,6 @@ export interface components { /** Format: int64 */ outputTokens?: number; }; - PromptResult: { - parsedJson?: components["schemas"]["JsonNode"]; - /** Format: int32 */ - price: number; - response: string; - usage?: components["schemas"]["PromptResponseUsageDto"]; - }; PromptRunDto: { basicPromptOptions?: ( | "KEY_NAME" @@ -5945,6 +5925,8 @@ export interface components { globalServerRole: "USER" | "ADMIN"; /** Format: int64 */ id: number; + /** Format: date-time */ + lastActivity?: string; mfaEnabled: boolean; name?: string; username: string; @@ -19320,45 +19302,6 @@ export interface operations { }; }; }; - prompt: { - responses: { - /** OK */ - 200: { - content: { - "application/json": components["schemas"]["PromptResult"]; - }; - }; - /** Bad Request */ - 400: { - content: { - "application/json": string; - }; - }; - /** Unauthorized */ - 401: { - content: { - "application/json": string; - }; - }; - /** Forbidden */ - 403: { - content: { - "application/json": string; - }; - }; - /** Not Found */ - 404: { - content: { - "application/json": string; - }; - }; - }; - requestBody: { - content: { - "application/json": components["schemas"]["LlmParams"]; - }; - }; - }; /** Get machine translation providers */ getInfo_4: { responses: { diff --git a/webapp/src/views/administration/AdministrationUsers.tsx b/webapp/src/views/administration/AdministrationUsers.tsx index 6c07352b86..fa4aeebcdb 100644 --- a/webapp/src/views/administration/AdministrationUsers.tsx +++ b/webapp/src/views/administration/AdministrationUsers.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { useTranslate } from '@tolgee/react'; -import { Box, Chip, ListItem, ListItemText, styled } from '@mui/material'; +import { Box, Chip, ListItem, ListItemText, Typography, styled } from '@mui/material'; import { PaginatedHateoasList } from 'tg.component/common/list/PaginatedHateoasList'; import { DashboardPage } from 'tg.component/layout/DashboardPage'; @@ -71,7 +71,19 @@ export const AdministrationUsers = ({ sx={{ display: 'grid', gridTemplateColumns: '1fr auto' }} > - {u.name} | {u.username} + + {u.name} | {u.username} + + + {u.lastActivity + ? `Last Activity: ${new Date(u.lastActivity).toLocaleString()}` + : "No activity yet" + } + {u.mfaEnabled && }