Skip to content

Commit 7de81df

Browse files
Merge branch 'develop' of https://github.com/hcengineering/platform into staging-new
2 parents 2daa162 + 1cd34ea commit 7de81df

File tree

17 files changed

+233
-28
lines changed

17 files changed

+233
-28
lines changed

.github/workflows/main.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ jobs:
245245
run: node common/scripts/install-run-rush.js docker
246246
env:
247247
DOCKER_CLI_HINTS: false
248+
DOCKER_BUILDKIT: 1
248249
- name: Configure /etc/hosts
249250
run: |
250251
sudo echo "127.0.0.1 huly.local" | sudo tee -a /etc/hosts
@@ -375,6 +376,7 @@ jobs:
375376
run: node common/scripts/install-run-rush.js docker
376377
env:
377378
DOCKER_CLI_HINTS: false
379+
DOCKER_BUILDKIT: 1
378380
- name: Configure /etc/hosts
379381
run: |
380382
sudo echo "127.0.0.1 huly.local" | sudo tee -a /etc/hosts
@@ -469,6 +471,7 @@ jobs:
469471
run: node common/scripts/install-run-rush.js docker
470472
env:
471473
DOCKER_CLI_HINTS: false
474+
DOCKER_BUILDKIT: 1
472475
- name: Configure /etc/hosts
473476
run: |
474477
sudo echo "127.0.0.1 huly.local" | sudo tee -a /etc/hosts
@@ -550,6 +553,7 @@ jobs:
550553
run: node common/scripts/install-run-rush.js docker
551554
env:
552555
DOCKER_CLI_HINTS: false
556+
DOCKER_BUILDKIT: 1
553557
- name: Configure /etc/hosts
554558
run: |
555559
sudo echo "127.0.0.1 huly.local" | sudo tee -a /etc/hosts
@@ -674,6 +678,8 @@ jobs:
674678
env:
675679
DOCKER_CLI_HINTS: false
676680
DOCKER_EXTRA: --platform=linux/amd64,linux/arm64
681+
DOCKER_BUILDKIT: 1
682+
DOCKER_BUILD_CLEANUP: true
677683
- name: Docker build love-agent
678684
run: |
679685
cd ./services/ai-bot/love-agent
@@ -682,6 +688,7 @@ jobs:
682688
env:
683689
DOCKER_CLI_HINTS: false
684690
DOCKER_EXTRA: --platform=linux/amd64,linux/arm64
691+
DOCKER_BUILDKIT: 1
685692
- name: Login to Docker Hub
686693
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/tags/s') }}
687694
uses: docker/login-action@v3

common/scripts/docker_build.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,33 @@
22

33
version=$(git rev-parse HEAD)
44

5+
# Check for cleanup flag from environment
6+
cleanup=false
7+
if [ "$DOCKER_BUILD_CLEANUP" = "true" ]; then
8+
cleanup=true
9+
fi
10+
511
echo "Building version: $version"
612

713
docker build -t "$1" -t "$1:$version" ${DOCKER_EXTRA} .
14+
15+
if [ "$cleanup" = true ]; then
16+
echo "Cleaning up build artifacts..."
17+
18+
if [ -d "bundle" ]; then
19+
echo " Removing bundle/"
20+
rm -rf bundle
21+
fi
22+
23+
if [ -d "dist" ]; then
24+
echo " Removing dist/"
25+
rm -rf dist
26+
fi
27+
28+
if [ -d ".rush" ]; then
29+
echo " Removing .rush/"
30+
rm -rf .rush
31+
fi
32+
33+
echo " Size after cleanup: $(du -sh . 2>/dev/null | cut -f1)"
34+
fi

dev/tool/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM hardcoreeng/base
1+
FROM hardcoreeng/base-slim:v20250916
22
WORKDIR /usr/src/app
33

44
COPY bundle/bundle.js ./

plugins/login-resources/src/components/Form.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@
4545
export let withProviders: boolean = false
4646
export let subtitle: string | undefined = undefined
4747
export let signUpDisabled = false
48+
export let isLoading: boolean = false
4849
4950
const validate = makeSequential(async function validateAsync (language: string): Promise<boolean> {
50-
if (ignoreInitialValidation) return true
51+
if (ignoreInitialValidation || isLoading) return true
5152
5253
for (const field of fields) {
5354
const v = object[field.name]

plugins/login-resources/src/components/LoginPasswordForm.svelte

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,24 +48,30 @@
4848
}
4949
5050
let status = OK
51+
let isLoading = false
5152
5253
const action = {
5354
i18n: login.string.LogIn,
5455
func: async () => {
55-
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
56-
const [loginStatus, result] = await doLogin(object.username, object.password)
57-
status = loginStatus
56+
isLoading = true
57+
try {
58+
status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
59+
const [loginStatus, result] = await doLogin(object.username, object.password)
60+
status = loginStatus
5861
59-
if (onLogin !== undefined) {
60-
void onLogin(result, status)
61-
} else {
62-
await doLoginNavigate(
63-
result,
64-
(st) => {
65-
status = st
66-
},
67-
navigateUrl
68-
)
62+
if (onLogin !== undefined) {
63+
void onLogin(result, status)
64+
} else {
65+
await doLoginNavigate(
66+
result,
67+
(st) => {
68+
status = st
69+
},
70+
navigateUrl
71+
)
72+
}
73+
} finally {
74+
isLoading = false
6975
}
7076
}
7177
}
@@ -79,6 +85,7 @@
7985
{object}
8086
{action}
8187
{signUpDisabled}
88+
{isLoading}
8289
bottomActions={[recoveryAction]}
8390
ignoreInitialValidation
8491
withProviders

qms-tests/sanity/tests/model/login-page.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export class LoginPage {
2626
async login (email: string, password: string): Promise<void> {
2727
await this.inputEmail.fill(email)
2828
await this.inputPassword.fill(password)
29+
30+
// Wait for form validation to complete
31+
await this.page.waitForTimeout(1000)
32+
2933
expect(await this.buttonLogin.isEnabled()).toBe(true)
3034
await this.buttonLogin.click()
3135
}

server-plugins/notification-resources/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,12 @@ async function getValueCollaborators (value: any, attr: AnyAttribute, control: T
256256
if (arrOf._class === core.class.RefTo) {
257257
const to = (arrOf as RefTo<Doc>).to
258258
if (hierarchy.isDerived(to, contact.class.Person)) {
259+
if (!Array.isArray(value)) {
260+
control.ctx.error('Expected array but got non-array value when getting value collaborators', { attr, value })
261+
return []
262+
}
259263
if (value.length === 0) return []
260-
if ((value as any[]).every((it) => it === null)) {
264+
if (value.every((it) => it === null)) {
261265
control.ctx.error('Null-values array of person refs when getting value collaborators', { attr, value })
262266
}
263267

server/account/src/__tests__/utils.test.ts

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,16 @@ import {
6565
loginOrSignUpWithProvider,
6666
sendEmail,
6767
addSocialIdBase,
68-
doReleaseSocialId
68+
doReleaseSocialId,
69+
getLastPasswordChangeEvent,
70+
isPasswordChangedSince
6971
} from '../utils'
7072
// eslint-disable-next-line import/no-named-default
7173
import platform, { getMetadata, PlatformError, Severity, Status } from '@hcengineering/platform'
7274
import { decodeTokenVerbose, generateToken, TokenError } from '@hcengineering/server-token'
7375
import { randomBytes } from 'crypto'
7476

75-
import { type AccountDB, AccountEventType, type Workspace } from '../types'
77+
import { type AccountDB, type AccountEvent, AccountEventType, type Workspace } from '../types'
7678
import { accountPlugin } from '../plugin'
7779

7880
// Mock platform with minimum required functionality
@@ -514,6 +516,110 @@ describe('account utils', () => {
514516
expect(verifyPassword(password, hash, salt)).toBe(false)
515517
})
516518
})
519+
520+
describe('getLastPasswordChangeEvent', () => {
521+
const mockDb = {
522+
accountEvent: {
523+
find: jest.fn() as jest.MockedFunction<AccountDB['accountEvent']['find']>
524+
}
525+
} as unknown as AccountDB
526+
527+
beforeEach(() => {
528+
jest.clearAllMocks()
529+
})
530+
531+
test('should return most recent password change event when it exists', async () => {
532+
const accountUuid = 'test-account-uuid' as AccountUuid
533+
const now = Date.now()
534+
const mockEvent: AccountEvent = {
535+
accountUuid,
536+
eventType: AccountEventType.PASSWORD_CHANGED,
537+
time: now
538+
}
539+
540+
;(mockDb.accountEvent.find as jest.Mock).mockResolvedValue([mockEvent])
541+
542+
const result = await getLastPasswordChangeEvent(mockDb, accountUuid)
543+
544+
expect(result).toEqual(mockEvent)
545+
expect(mockDb.accountEvent.find).toHaveBeenCalledWith(
546+
{ accountUuid, eventType: AccountEventType.PASSWORD_CHANGED },
547+
{ time: 'descending' },
548+
1
549+
)
550+
})
551+
552+
test('should return null when no password change events exist', async () => {
553+
const accountUuid = 'test-account-uuid' as AccountUuid
554+
555+
;(mockDb.accountEvent.find as jest.Mock).mockResolvedValue([])
556+
557+
const result = await getLastPasswordChangeEvent(mockDb, accountUuid)
558+
559+
expect(result).toBeNull()
560+
})
561+
})
562+
563+
describe('isPasswordChangedSince', () => {
564+
const mockDb = {
565+
accountEvent: {
566+
find: jest.fn() as jest.MockedFunction<AccountDB['accountEvent']['find']>
567+
}
568+
} as unknown as AccountDB
569+
570+
beforeEach(() => {
571+
jest.clearAllMocks()
572+
})
573+
574+
test('should return true when password changed after given timestamp', async () => {
575+
const accountUuid = 'test-account-uuid' as AccountUuid
576+
const now = Date.now()
577+
const oneHourAgo = now - 1000 * 60 * 60 // 1 hour ago
578+
const halfHourAgo = now - 1000 * 60 * 30 // 30 min ago
579+
580+
const mockEvent: AccountEvent = {
581+
accountUuid,
582+
eventType: AccountEventType.PASSWORD_CHANGED,
583+
time: halfHourAgo
584+
}
585+
586+
;(mockDb.accountEvent.find as jest.Mock).mockResolvedValue([mockEvent])
587+
588+
const result = await isPasswordChangedSince(mockDb, accountUuid, oneHourAgo)
589+
590+
expect(result).toBe(true)
591+
})
592+
593+
test('should return false when password changed before given timestamp', async () => {
594+
const accountUuid = 'test-account-uuid' as AccountUuid
595+
const now = Date.now()
596+
const oneMonthAgo = now - 1000 * 60 * 60 * 24 * 30 // 1 month ago
597+
const twoMonthsAgo = now - 1000 * 60 * 60 * 24 * 60 * 2 // 2 months ago
598+
599+
const mockEvent: AccountEvent = {
600+
accountUuid,
601+
eventType: AccountEventType.PASSWORD_CHANGED,
602+
time: twoMonthsAgo
603+
}
604+
605+
;(mockDb.accountEvent.find as jest.Mock).mockResolvedValue([mockEvent])
606+
607+
const result = await isPasswordChangedSince(mockDb, accountUuid, oneMonthAgo)
608+
609+
expect(result).toBe(false)
610+
})
611+
612+
test('should return false when no password change events exist', async () => {
613+
const accountUuid = 'test-account-uuid' as AccountUuid
614+
const now = Date.now()
615+
616+
;(mockDb.accountEvent.find as jest.Mock).mockResolvedValue([])
617+
618+
const result = await isPasswordChangedSince(mockDb, accountUuid, now)
619+
620+
expect(result).toBe(false)
621+
})
622+
})
517623
})
518624

519625
describe('wrap', () => {

server/account/src/collections/postgres/migrations.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ export function getMigrations (ns: string): [string, string][] {
3939
getV18Migration(ns),
4040
getV19Migration(ns),
4141
getV20Migration(ns),
42-
getV21Migration(ns)
42+
getV21Migration(ns),
43+
getV22Migration(ns)
4344
]
4445
}
4546

@@ -579,3 +580,13 @@ function getV21Migration (ns: string): [string, string] {
579580
`
580581
]
581582
}
583+
584+
function getV22Migration (ns: string): [string, string] {
585+
return [
586+
'account_db_v22_add_password_change_event_index',
587+
`
588+
CREATE INDEX IF NOT EXISTS account_events_account_uuid_event_type_time_idx
589+
ON ${ns}.account_events (account_uuid, event_type, time DESC);
590+
`
591+
]
592+
}

server/account/src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ export interface AccountEvent {
7878
export enum AccountEventType {
7979
ACCOUNT_CREATED = 'account_created',
8080
SOCIAL_ID_RELEASED = 'social_id_released',
81-
ACCOUNT_DELETED = 'account_deleted'
81+
ACCOUNT_DELETED = 'account_deleted',
82+
PASSWORD_CHANGED = 'password_changed'
8283
}
8384

8485
export interface Member {

0 commit comments

Comments
 (0)