From 8459e7d1f8631abaaa0ad2cf21f7e8e4dd7f5024 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Fri, 14 Nov 2025 14:59:04 +0700 Subject: [PATCH 1/6] Fix QMS login tests (#10210) Signed-off-by: Artem Savchenko --- .../src/components/Form.svelte | 3 +- .../src/components/LoginPasswordForm.svelte | 33 +++++++++++-------- qms-tests/sanity/tests/model/login-page.ts | 4 +++ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/plugins/login-resources/src/components/Form.svelte b/plugins/login-resources/src/components/Form.svelte index eff31c5e73..3ba6096c02 100644 --- a/plugins/login-resources/src/components/Form.svelte +++ b/plugins/login-resources/src/components/Form.svelte @@ -45,9 +45,10 @@ export let withProviders: boolean = false export let subtitle: string | undefined = undefined export let signUpDisabled = false + export let isLoading: boolean = false const validate = makeSequential(async function validateAsync (language: string): Promise { - if (ignoreInitialValidation) return true + if (ignoreInitialValidation || isLoading) return true for (const field of fields) { const v = object[field.name] diff --git a/plugins/login-resources/src/components/LoginPasswordForm.svelte b/plugins/login-resources/src/components/LoginPasswordForm.svelte index 8237fc86bc..34fa12a725 100644 --- a/plugins/login-resources/src/components/LoginPasswordForm.svelte +++ b/plugins/login-resources/src/components/LoginPasswordForm.svelte @@ -48,24 +48,30 @@ } let status = OK + let isLoading = false const action = { i18n: login.string.LogIn, func: async () => { - status = new Status(Severity.INFO, login.status.ConnectingToServer, {}) - const [loginStatus, result] = await doLogin(object.username, object.password) - status = loginStatus + isLoading = true + try { + status = new Status(Severity.INFO, login.status.ConnectingToServer, {}) + const [loginStatus, result] = await doLogin(object.username, object.password) + status = loginStatus - if (onLogin !== undefined) { - void onLogin(result, status) - } else { - await doLoginNavigate( - result, - (st) => { - status = st - }, - navigateUrl - ) + if (onLogin !== undefined) { + void onLogin(result, status) + } else { + await doLoginNavigate( + result, + (st) => { + status = st + }, + navigateUrl + ) + } + } finally { + isLoading = false } } } @@ -79,6 +85,7 @@ {object} {action} {signUpDisabled} + {isLoading} bottomActions={[recoveryAction]} ignoreInitialValidation withProviders diff --git a/qms-tests/sanity/tests/model/login-page.ts b/qms-tests/sanity/tests/model/login-page.ts index 153ec48035..7445202a3d 100644 --- a/qms-tests/sanity/tests/model/login-page.ts +++ b/qms-tests/sanity/tests/model/login-page.ts @@ -26,6 +26,10 @@ export class LoginPage { async login (email: string, password: string): Promise { await this.inputEmail.fill(email) await this.inputPassword.fill(password) + + // Wait for form validation to complete + await this.page.waitForTimeout(1000) + expect(await this.buttonLogin.isEnabled()).toBe(true) await this.buttonLogin.click() } From 02fa0f4419affadf9082763e6c4d0c93de11ba81 Mon Sep 17 00:00:00 2001 From: Alexey Zinoviev Date: Mon, 17 Nov 2025 09:09:49 +0400 Subject: [PATCH 2/6] eqms-1401: track password changed events (#10217) Signed-off-by: Alexey Zinoviev --- server/account/src/__tests__/utils.test.ts | 110 +++++++++++++++++- .../src/collections/postgres/migrations.ts | 13 ++- server/account/src/types.ts | 3 +- server/account/src/utils.ts | 33 +++++- 4 files changed, 153 insertions(+), 6 deletions(-) diff --git a/server/account/src/__tests__/utils.test.ts b/server/account/src/__tests__/utils.test.ts index dd18a62c21..6a86da0799 100644 --- a/server/account/src/__tests__/utils.test.ts +++ b/server/account/src/__tests__/utils.test.ts @@ -65,14 +65,16 @@ import { loginOrSignUpWithProvider, sendEmail, addSocialIdBase, - doReleaseSocialId + doReleaseSocialId, + getLastPasswordChangeEvent, + isPasswordChangedSince } from '../utils' // eslint-disable-next-line import/no-named-default import platform, { getMetadata, PlatformError, Severity, Status } from '@hcengineering/platform' import { decodeTokenVerbose, generateToken, TokenError } from '@hcengineering/server-token' import { randomBytes } from 'crypto' -import { type AccountDB, AccountEventType, type Workspace } from '../types' +import { type AccountDB, type AccountEvent, AccountEventType, type Workspace } from '../types' import { accountPlugin } from '../plugin' // Mock platform with minimum required functionality @@ -514,6 +516,110 @@ describe('account utils', () => { expect(verifyPassword(password, hash, salt)).toBe(false) }) }) + + describe('getLastPasswordChangeEvent', () => { + const mockDb = { + accountEvent: { + find: jest.fn() as jest.MockedFunction + } + } as unknown as AccountDB + + beforeEach(() => { + jest.clearAllMocks() + }) + + test('should return most recent password change event when it exists', async () => { + const accountUuid = 'test-account-uuid' as AccountUuid + const now = Date.now() + const mockEvent: AccountEvent = { + accountUuid, + eventType: AccountEventType.PASSWORD_CHANGED, + time: now + } + + ;(mockDb.accountEvent.find as jest.Mock).mockResolvedValue([mockEvent]) + + const result = await getLastPasswordChangeEvent(mockDb, accountUuid) + + expect(result).toEqual(mockEvent) + expect(mockDb.accountEvent.find).toHaveBeenCalledWith( + { accountUuid, eventType: AccountEventType.PASSWORD_CHANGED }, + { time: 'descending' }, + 1 + ) + }) + + test('should return null when no password change events exist', async () => { + const accountUuid = 'test-account-uuid' as AccountUuid + + ;(mockDb.accountEvent.find as jest.Mock).mockResolvedValue([]) + + const result = await getLastPasswordChangeEvent(mockDb, accountUuid) + + expect(result).toBeNull() + }) + }) + + describe('isPasswordChangedSince', () => { + const mockDb = { + accountEvent: { + find: jest.fn() as jest.MockedFunction + } + } as unknown as AccountDB + + beforeEach(() => { + jest.clearAllMocks() + }) + + test('should return true when password changed after given timestamp', async () => { + const accountUuid = 'test-account-uuid' as AccountUuid + const now = Date.now() + const oneHourAgo = now - 1000 * 60 * 60 // 1 hour ago + const halfHourAgo = now - 1000 * 60 * 30 // 30 min ago + + const mockEvent: AccountEvent = { + accountUuid, + eventType: AccountEventType.PASSWORD_CHANGED, + time: halfHourAgo + } + + ;(mockDb.accountEvent.find as jest.Mock).mockResolvedValue([mockEvent]) + + const result = await isPasswordChangedSince(mockDb, accountUuid, oneHourAgo) + + expect(result).toBe(true) + }) + + test('should return false when password changed before given timestamp', async () => { + const accountUuid = 'test-account-uuid' as AccountUuid + const now = Date.now() + const oneMonthAgo = now - 1000 * 60 * 60 * 24 * 30 // 1 month ago + const twoMonthsAgo = now - 1000 * 60 * 60 * 24 * 60 * 2 // 2 months ago + + const mockEvent: AccountEvent = { + accountUuid, + eventType: AccountEventType.PASSWORD_CHANGED, + time: twoMonthsAgo + } + + ;(mockDb.accountEvent.find as jest.Mock).mockResolvedValue([mockEvent]) + + const result = await isPasswordChangedSince(mockDb, accountUuid, oneMonthAgo) + + expect(result).toBe(false) + }) + + test('should return false when no password change events exist', async () => { + const accountUuid = 'test-account-uuid' as AccountUuid + const now = Date.now() + + ;(mockDb.accountEvent.find as jest.Mock).mockResolvedValue([]) + + const result = await isPasswordChangedSince(mockDb, accountUuid, now) + + expect(result).toBe(false) + }) + }) }) describe('wrap', () => { diff --git a/server/account/src/collections/postgres/migrations.ts b/server/account/src/collections/postgres/migrations.ts index f03bd68bef..be4a5d5778 100644 --- a/server/account/src/collections/postgres/migrations.ts +++ b/server/account/src/collections/postgres/migrations.ts @@ -39,7 +39,8 @@ export function getMigrations (ns: string): [string, string][] { getV18Migration(ns), getV19Migration(ns), getV20Migration(ns), - getV21Migration(ns) + getV21Migration(ns), + getV22Migration(ns) ] } @@ -579,3 +580,13 @@ function getV21Migration (ns: string): [string, string] { ` ] } + +function getV22Migration (ns: string): [string, string] { + return [ + 'account_db_v22_add_password_change_event_index', + ` + CREATE INDEX IF NOT EXISTS account_events_account_uuid_event_type_time_idx + ON ${ns}.account_events (account_uuid, event_type, time DESC); + ` + ] +} diff --git a/server/account/src/types.ts b/server/account/src/types.ts index b5629780b6..05676ec5d0 100644 --- a/server/account/src/types.ts +++ b/server/account/src/types.ts @@ -78,7 +78,8 @@ export interface AccountEvent { export enum AccountEventType { ACCOUNT_CREATED = 'account_created', SOCIAL_ID_RELEASED = 'social_id_released', - ACCOUNT_DELETED = 'account_deleted' + ACCOUNT_DELETED = 'account_deleted', + PASSWORD_CHANGED = 'password_changed' } export interface Member { diff --git a/server/account/src/utils.ts b/server/account/src/utils.ts index 3ebf6e9c0d..e6be70ad8d 100644 --- a/server/account/src/utils.ts +++ b/server/account/src/utils.ts @@ -48,6 +48,7 @@ import { accountPlugin } from './plugin' import { type Account, type AccountDB, + type AccountEvent, AccountEventType, type AccountMethodHandler, type Integration, @@ -428,7 +429,7 @@ export async function setPassword ( ctx: MeasureContext, db: AccountDB, branding: Branding | null, - personUuid: AccountUuid, + accountUuid: AccountUuid, password: string ): Promise { if (password == null || password === '') { @@ -436,7 +437,35 @@ export async function setPassword ( } const salt = randomBytes(32) - await db.setPassword(personUuid, hashWithSalt(password, salt), salt) + await db.setPassword(accountUuid, hashWithSalt(password, salt), salt) + + // Record password change event + try { + await db.accountEvent.insertOne({ + accountUuid, + eventType: AccountEventType.PASSWORD_CHANGED, + time: Date.now() + }) + } catch (err) { + ctx.warn('Failed to record password change event', { accountUuid, err }) + } +} + +export async function getLastPasswordChangeEvent ( + db: AccountDB, + accountUuid: AccountUuid +): Promise { + const result = await db.accountEvent.find( + { accountUuid, eventType: AccountEventType.PASSWORD_CHANGED }, + { time: 'descending' }, + 1 + ) + return result[0] ?? null +} + +export async function isPasswordChangedSince (db: AccountDB, accountUuid: AccountUuid, since: number): Promise { + const lastEvent = await getLastPasswordChangeEvent(db, accountUuid) + return lastEvent != null && lastEvent.time >= since } export async function generateUniqueOtp (db: AccountDB): Promise { From a8a4b90d4ebe5c93459b02be473b42304255a6ac Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Mon, 17 Nov 2025 12:39:34 +0700 Subject: [PATCH 3/6] Fix value.every is not a function (#10213) Signed-off-by: Artem Savchenko --- server-plugins/notification-resources/src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index c7ad799ff3..9df493219e 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -256,8 +256,12 @@ async function getValueCollaborators (value: any, attr: AnyAttribute, control: T if (arrOf._class === core.class.RefTo) { const to = (arrOf as RefTo).to if (hierarchy.isDerived(to, contact.class.Person)) { + if (!Array.isArray(value)) { + control.ctx.error('Expected array but got non-array value when getting value collaborators', { attr, value }) + return [] + } if (value.length === 0) return [] - if ((value as any[]).every((it) => it === null)) { + if (value.every((it) => it === null)) { control.ctx.error('Null-values array of person refs when getting value collaborators', { attr, value }) } From 0592bf5815966a7d1194bf84ff0642563d488e15 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Mon, 17 Nov 2025 13:34:20 +0700 Subject: [PATCH 4/6] Fix bandwidth constraint errors (#10211) * Fix bandwidth constraint errors Signed-off-by: Artem Savchenko * Clean up Signed-off-by: Artem Savchenko --------- Signed-off-by: Artem Savchenko --- services/billing/pod-billing/src/db/migrations.ts | 10 +++++++++- services/billing/pod-billing/src/db/postgres.ts | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/services/billing/pod-billing/src/db/migrations.ts b/services/billing/pod-billing/src/db/migrations.ts index e2a3921343..97539332c9 100644 --- a/services/billing/pod-billing/src/db/migrations.ts +++ b/services/billing/pod-billing/src/db/migrations.ts @@ -14,7 +14,7 @@ // export function getMigrations (): [string, string][] { - return [migrationV1(), migrationV2()] + return [migrationV1(), migrationV2(), migrationV3()] } function migrationV1 (): [string, string] { @@ -77,3 +77,11 @@ function migrationV2 (): [string, string] { ` return ['init_ai_usage_tables_02', sql] } + +function migrationV3 (): [string, string] { + const sql = ` + UPDATE billing.livekit_session SET bandwidth = 0 WHERE bandwidth IS NULL; + ALTER TABLE billing.livekit_session ALTER COLUMN bandwidth SET DEFAULT 0; + ` + return ['fix_bandwidth_nulls_03', sql] +} diff --git a/services/billing/pod-billing/src/db/postgres.ts b/services/billing/pod-billing/src/db/postgres.ts index 557e747cd7..bf0b13ebaa 100644 --- a/services/billing/pod-billing/src/db/postgres.ts +++ b/services/billing/pod-billing/src/db/postgres.ts @@ -204,7 +204,7 @@ class PostgresDB implements BillingDB { values.push( `($${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++})` ) - params.push(workspace, sessionId, sessionStart, sessionEnd, room, bandwidth, minutes) + params.push(workspace, sessionId, sessionStart, sessionEnd, room, bandwidth ?? 0, minutes) } if (values.length === 0) continue From 01ebea2a99ebc5ffc4294b74eaa765497b958da0 Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Mon, 17 Nov 2025 21:49:25 +0700 Subject: [PATCH 5/6] fix: use slim docker image (#10214) Signed-off-by: Alexander Onnikov --- dev/tool/Dockerfile | 2 +- services/backup/backup-api-pod/Dockerfile | 2 +- services/billing/pod-billing/Dockerfile | 2 +- services/export/pod-export/Dockerfile | 2 +- services/payment/pod-payment/Dockerfile | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dev/tool/Dockerfile b/dev/tool/Dockerfile index c222852188..7079536dbc 100644 --- a/dev/tool/Dockerfile +++ b/dev/tool/Dockerfile @@ -1,4 +1,4 @@ -FROM hardcoreeng/base +FROM hardcoreeng/base-slim:v20250916 WORKDIR /usr/src/app COPY bundle/bundle.js ./ diff --git a/services/backup/backup-api-pod/Dockerfile b/services/backup/backup-api-pod/Dockerfile index ebb6e0c8e8..e74c1c8c5a 100644 --- a/services/backup/backup-api-pod/Dockerfile +++ b/services/backup/backup-api-pod/Dockerfile @@ -1,4 +1,4 @@ -FROM hardcoreeng/front-base:v20250916 +FROM hardcoreeng/base-slim:v20250916 WORKDIR /app COPY bundle/bundle.js ./ diff --git a/services/billing/pod-billing/Dockerfile b/services/billing/pod-billing/Dockerfile index 15a8d24dda..e74c1c8c5a 100644 --- a/services/billing/pod-billing/Dockerfile +++ b/services/billing/pod-billing/Dockerfile @@ -1,4 +1,4 @@ -FROM hardcoreeng/base:v20250916 +FROM hardcoreeng/base-slim:v20250916 WORKDIR /app COPY bundle/bundle.js ./ diff --git a/services/export/pod-export/Dockerfile b/services/export/pod-export/Dockerfile index 5c96aa8e47..1a121895ba 100644 --- a/services/export/pod-export/Dockerfile +++ b/services/export/pod-export/Dockerfile @@ -1,5 +1,5 @@ -FROM hardcoreeng/base:v20250916 +FROM hardcoreeng/base-slim:v20250916 WORKDIR /usr/src/app COPY bundle/bundle.js ./ diff --git a/services/payment/pod-payment/Dockerfile b/services/payment/pod-payment/Dockerfile index 15a8d24dda..e74c1c8c5a 100644 --- a/services/payment/pod-payment/Dockerfile +++ b/services/payment/pod-payment/Dockerfile @@ -1,4 +1,4 @@ -FROM hardcoreeng/base:v20250916 +FROM hardcoreeng/base-slim:v20250916 WORKDIR /app COPY bundle/bundle.js ./ From 1cd34ea3537a8de03eadcd3cbb18242517851924 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Mon, 17 Nov 2025 21:50:06 +0700 Subject: [PATCH 6/6] Optimise docker build space usage (#10224) * Use BuildKit builder instead of legacy one Signed-off-by: Artem Savchenko * fix: use slim docker image Signed-off-by: Alexander Onnikov * Add dockerignore Signed-off-by: Artem Savchenko * Clean up build artifacts Signed-off-by: Artem Savchenko * Clean up Signed-off-by: Artem Savchenko * Fix flag Signed-off-by: Artem Savchenko --------- Signed-off-by: Artem Savchenko Signed-off-by: Alexander Onnikov Co-authored-by: Alexander Onnikov --- .github/workflows/main.yml | 7 +++++++ common/scripts/docker_build.sh | 27 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1c5edf3d21..bd6150a0d0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -245,6 +245,7 @@ jobs: run: node common/scripts/install-run-rush.js docker env: DOCKER_CLI_HINTS: false + DOCKER_BUILDKIT: 1 - name: Configure /etc/hosts run: | sudo echo "127.0.0.1 huly.local" | sudo tee -a /etc/hosts @@ -375,6 +376,7 @@ jobs: run: node common/scripts/install-run-rush.js docker env: DOCKER_CLI_HINTS: false + DOCKER_BUILDKIT: 1 - name: Configure /etc/hosts run: | sudo echo "127.0.0.1 huly.local" | sudo tee -a /etc/hosts @@ -469,6 +471,7 @@ jobs: run: node common/scripts/install-run-rush.js docker env: DOCKER_CLI_HINTS: false + DOCKER_BUILDKIT: 1 - name: Configure /etc/hosts run: | sudo echo "127.0.0.1 huly.local" | sudo tee -a /etc/hosts @@ -550,6 +553,7 @@ jobs: run: node common/scripts/install-run-rush.js docker env: DOCKER_CLI_HINTS: false + DOCKER_BUILDKIT: 1 - name: Configure /etc/hosts run: | sudo echo "127.0.0.1 huly.local" | sudo tee -a /etc/hosts @@ -674,6 +678,8 @@ jobs: env: DOCKER_CLI_HINTS: false DOCKER_EXTRA: --platform=linux/amd64,linux/arm64 + DOCKER_BUILDKIT: 1 + DOCKER_BUILD_CLEANUP: true - name: Docker build love-agent run: | cd ./services/ai-bot/love-agent @@ -682,6 +688,7 @@ jobs: env: DOCKER_CLI_HINTS: false DOCKER_EXTRA: --platform=linux/amd64,linux/arm64 + DOCKER_BUILDKIT: 1 - name: Login to Docker Hub if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/tags/s') }} uses: docker/login-action@v3 diff --git a/common/scripts/docker_build.sh b/common/scripts/docker_build.sh index 586eb01750..2a564dc375 100755 --- a/common/scripts/docker_build.sh +++ b/common/scripts/docker_build.sh @@ -2,6 +2,33 @@ version=$(git rev-parse HEAD) +# Check for cleanup flag from environment +cleanup=false +if [ "$DOCKER_BUILD_CLEANUP" = "true" ]; then + cleanup=true +fi + echo "Building version: $version" docker build -t "$1" -t "$1:$version" ${DOCKER_EXTRA} . + +if [ "$cleanup" = true ]; then + echo "Cleaning up build artifacts..." + + if [ -d "bundle" ]; then + echo " Removing bundle/" + rm -rf bundle + fi + + if [ -d "dist" ]; then + echo " Removing dist/" + rm -rf dist + fi + + if [ -d ".rush" ]; then + echo " Removing .rush/" + rm -rf .rush + fi + + echo " Size after cleanup: $(du -sh . 2>/dev/null | cut -f1)" +fi