mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-26 11:34:58 +02:00
Merge branch 'develop' of https://github.com/hcengineering/platform into staging-new
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM hardcoreeng/base
|
||||
FROM hardcoreeng/base-slim:v20250916
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY bundle/bundle.js ./
|
||||
|
||||
@@ -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<boolean> {
|
||||
if (ignoreInitialValidation) return true
|
||||
if (ignoreInitialValidation || isLoading) return true
|
||||
|
||||
for (const field of fields) {
|
||||
const v = object[field.name]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,6 +26,10 @@ export class LoginPage {
|
||||
async login (email: string, password: string): Promise<void> {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -256,8 +256,12 @@ async function getValueCollaborators (value: any, attr: AnyAttribute, control: T
|
||||
if (arrOf._class === core.class.RefTo) {
|
||||
const to = (arrOf as RefTo<Doc>).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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AccountDB['accountEvent']['find']>
|
||||
}
|
||||
} 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<AccountDB['accountEvent']['find']>
|
||||
}
|
||||
} 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', () => {
|
||||
|
||||
@@ -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);
|
||||
`
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<void> {
|
||||
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<AccountEvent | null> {
|
||||
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<boolean> {
|
||||
const lastEvent = await getLastPasswordChangeEvent(db, accountUuid)
|
||||
return lastEvent != null && lastEvent.time >= since
|
||||
}
|
||||
|
||||
export async function generateUniqueOtp (db: AccountDB): Promise<string> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM hardcoreeng/front-base:v20250916
|
||||
FROM hardcoreeng/base-slim:v20250916
|
||||
WORKDIR /app
|
||||
|
||||
COPY bundle/bundle.js ./
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM hardcoreeng/base:v20250916
|
||||
FROM hardcoreeng/base-slim:v20250916
|
||||
WORKDIR /app
|
||||
|
||||
COPY bundle/bundle.js ./
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
FROM hardcoreeng/base:v20250916
|
||||
FROM hardcoreeng/base-slim:v20250916
|
||||
WORKDIR /usr/src/app
|
||||
COPY bundle/bundle.js ./
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM hardcoreeng/base:v20250916
|
||||
FROM hardcoreeng/base-slim:v20250916
|
||||
WORKDIR /app
|
||||
|
||||
COPY bundle/bundle.js ./
|
||||
|
||||
Reference in New Issue
Block a user