Final
This commit is contained in:
1 parent
fc0dfd0556
commit
649dd6f028
20 files changed
+3725
-988
No files matched your search
@@ -17,8 +17,11 @@ export type AuthUser = {
|
||||
active: boolean;
|
||||
zammad_user_id: string | null;
|
||||
has_zammad_api_token: boolean;
|
||||
teamspace_user_id: string | null;
|
||||
has_teamspace_credentials: boolean;
|
||||
zammad_email_session_webhook_enabled: boolean;
|
||||
session_billing_prompt_enabled: boolean;
|
||||
teamspace_time_tracking_enabled: boolean;
|
||||
zammad_session_note_default_enabled: boolean;
|
||||
zammad_time_unit_rounding_enabled: boolean;
|
||||
zammad_time_unit_rounding_minutes: number;
|
||||
@@ -90,8 +93,11 @@ function publicUser(user: AuthUser) {
|
||||
active: user.active,
|
||||
zammad_user_id: user.zammad_user_id ?? null,
|
||||
has_zammad_api_token: Boolean(user.has_zammad_api_token),
|
||||
teamspace_user_id: user.teamspace_user_id ?? null,
|
||||
has_teamspace_credentials: Boolean(user.has_teamspace_credentials),
|
||||
zammad_email_session_webhook_enabled: Boolean(user.zammad_email_session_webhook_enabled),
|
||||
session_billing_prompt_enabled: Boolean(user.session_billing_prompt_enabled),
|
||||
teamspace_time_tracking_enabled: Boolean(user.teamspace_time_tracking_enabled ?? true),
|
||||
zammad_session_note_default_enabled: Boolean(user.zammad_session_note_default_enabled),
|
||||
zammad_time_unit_rounding_enabled: Boolean(user.zammad_time_unit_rounding_enabled),
|
||||
zammad_time_unit_rounding_minutes: Number(user.zammad_time_unit_rounding_minutes ?? 15),
|
||||
@@ -159,8 +165,11 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
|
||||
u.active,
|
||||
u.zammad_user_id::text AS zammad_user_id,
|
||||
(u.zammad_api_token IS NOT NULL) AS has_zammad_api_token,
|
||||
u.teamspace_user_id::text AS teamspace_user_id,
|
||||
(u.teamspace_device_id IS NOT NULL AND u.teamspace_device_password IS NOT NULL) AS has_teamspace_credentials,
|
||||
u.zammad_email_session_webhook_enabled,
|
||||
u.session_billing_prompt_enabled,
|
||||
u.teamspace_time_tracking_enabled,
|
||||
u.zammad_session_note_default_enabled,
|
||||
u.zammad_time_unit_rounding_enabled,
|
||||
u.zammad_time_unit_rounding_minutes,
|
||||
|
||||
+2082
-383
File diff suppressed because it is too large.
Load diff
+199
-15
@@ -55,6 +55,33 @@ async function encryptStoredApiSecrets() {
|
||||
}
|
||||
}
|
||||
|
||||
const teamspaceCredentialResult = await query<{ id: string; teamspace_device_id: string | null; teamspace_device_password: string | null }>(`
|
||||
SELECT id::text AS id, teamspace_device_id, teamspace_device_password
|
||||
FROM users
|
||||
WHERE teamspace_device_id IS NOT NULL
|
||||
OR teamspace_device_password IS NOT NULL;
|
||||
`);
|
||||
|
||||
for (const row of teamspaceCredentialResult.rows) {
|
||||
const updates: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
|
||||
if (row.teamspace_device_id && !isEncryptedSecret(row.teamspace_device_id)) {
|
||||
values.push(encryptSecret(row.teamspace_device_id));
|
||||
updates.push(`teamspace_device_id = $${values.length}`);
|
||||
}
|
||||
|
||||
if (row.teamspace_device_password && !isEncryptedSecret(row.teamspace_device_password)) {
|
||||
values.push(encryptSecret(row.teamspace_device_password));
|
||||
updates.push(`teamspace_device_password = $${values.length}`);
|
||||
}
|
||||
|
||||
if (updates.length > 0) {
|
||||
values.push(row.id);
|
||||
await query(`UPDATE users SET ${updates.join(", ")}, updated_at = now() WHERE id = $${values.length};`, values);
|
||||
}
|
||||
}
|
||||
|
||||
const zammadApiKeyResult = await query<{ value: string }>("SELECT value FROM app_settings WHERE key = $1;", [zammadApiKeySettingKey]);
|
||||
const zammadApiKey = zammadApiKeyResult.rows[0]?.value ?? null;
|
||||
|
||||
@@ -86,8 +113,12 @@ export async function migrate() {
|
||||
`);
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS zammad_api_token TEXT;");
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS zammad_user_id BIGINT;");
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS teamspace_device_id TEXT;");
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS teamspace_device_password TEXT;");
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS teamspace_user_id BIGINT;");
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS zammad_email_session_webhook_enabled BOOLEAN NOT NULL DEFAULT false;");
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS session_billing_prompt_enabled BOOLEAN NOT NULL DEFAULT true;");
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS teamspace_time_tracking_enabled BOOLEAN NOT NULL DEFAULT true;");
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS zammad_session_note_default_enabled BOOLEAN NOT NULL DEFAULT false;");
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS zammad_time_unit_rounding_enabled BOOLEAN NOT NULL DEFAULT false;");
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS zammad_time_unit_rounding_minutes INTEGER NOT NULL DEFAULT 15;");
|
||||
@@ -98,6 +129,7 @@ export async function migrate() {
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS project_followup_second_enabled BOOLEAN NOT NULL DEFAULT true;");
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS project_followup_second_weeks INTEGER NOT NULL DEFAULT 2;");
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS project_followup_complete_on_session BOOLEAN NOT NULL DEFAULT true;");
|
||||
await query("ALTER TABLE users DROP COLUMN IF EXISTS teamspace_subproject_assignment_enabled;");
|
||||
await query("UPDATE users SET zammad_article_type_id = 6 WHERE zammad_article_type_id IS NULL;");
|
||||
await query("ALTER TABLE users ALTER COLUMN zammad_article_type_id SET DEFAULT 6;");
|
||||
await query("ALTER TABLE users ALTER COLUMN zammad_article_type_id SET NOT NULL;");
|
||||
@@ -717,10 +749,11 @@ export async function migrate() {
|
||||
CONSTRAINT ticket_day_billings_minutes_non_negative CHECK (billed_minutes >= 0)
|
||||
);
|
||||
`);
|
||||
await query("ALTER TABLE ticket_day_billings ADD COLUMN IF NOT EXISTS project_teamspace_entry_id BIGINT;");
|
||||
await query("ALTER TABLE ticket_day_billings DROP CONSTRAINT IF EXISTS ticket_day_billings_pkey;");
|
||||
await query("CREATE UNIQUE INDEX IF NOT EXISTS idx_ticket_day_billings_ticket_user_day_default ON ticket_day_billings(ticket_id, user_id, day) WHERE project_teamspace_entry_id IS NULL;");
|
||||
await query("CREATE UNIQUE INDEX IF NOT EXISTS idx_ticket_day_billings_ticket_user_day_entry ON ticket_day_billings(ticket_id, user_id, day, project_teamspace_entry_id) WHERE project_teamspace_entry_id IS NOT NULL;");
|
||||
await query("ALTER TABLE ticket_day_billings ADD COLUMN IF NOT EXISTS project_teamspace_entry_id BIGINT;");
|
||||
await query("ALTER TABLE ticket_day_billings DROP CONSTRAINT IF EXISTS ticket_day_billings_pkey;");
|
||||
await query("DROP INDEX IF EXISTS idx_ticket_day_billings_ticket_user_day_default;");
|
||||
await query("DROP INDEX IF EXISTS idx_ticket_day_billings_ticket_user_day_entry;");
|
||||
await query("DROP INDEX IF EXISTS idx_ticket_day_billings_ticket_user_day_subentry;");
|
||||
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS ticket_day_billing_acknowledgements (
|
||||
@@ -829,8 +862,84 @@ export async function migrate() {
|
||||
|
||||
await query("ALTER TABLE project_teamspace_entries ADD COLUMN IF NOT EXISTS teamspace_description TEXT NOT NULL DEFAULT '';");
|
||||
await query("ALTER TABLE project_teamspace_entries ADD COLUMN IF NOT EXISTS budget_sync_disabled BOOLEAN NOT NULL DEFAULT false;");
|
||||
await query("ALTER TABLE project_teamspace_entries ADD COLUMN IF NOT EXISTS teamspace_api_project_id BIGINT;");
|
||||
await query("ALTER TABLE project_teamspace_entries ADD COLUMN IF NOT EXISTS teamspace_api_project_url TEXT;");
|
||||
await query("CREATE UNIQUE INDEX IF NOT EXISTS idx_project_teamspace_entries_default ON project_teamspace_entries(project_id) WHERE is_default;");
|
||||
await query("CREATE INDEX IF NOT EXISTS idx_project_teamspace_entries_project_order ON project_teamspace_entries(project_id, display_order, id);");
|
||||
await query("CREATE INDEX IF NOT EXISTS idx_project_teamspace_entries_ts_api_id ON project_teamspace_entries(teamspace_api_project_id) WHERE teamspace_api_project_id IS NOT NULL;");
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS project_teamspace_subentries (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
|
||||
project_teamspace_entry_id BIGINT NOT NULL REFERENCES project_teamspace_entries(id) ON DELETE CASCADE,
|
||||
teamspace_api_project_id BIGINT NOT NULL,
|
||||
teamspace_api_project_url TEXT,
|
||||
teamspace_project_id TEXT,
|
||||
billing_type TEXT,
|
||||
name TEXT,
|
||||
alias TEXT NOT NULL DEFAULT '',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
progress INTEGER,
|
||||
planned_minutes INTEGER,
|
||||
sum_minutes INTEGER,
|
||||
planned_minutes_billable INTEGER,
|
||||
sum_minutes_billable INTEGER,
|
||||
planned_income NUMERIC,
|
||||
sum_income NUMERIC,
|
||||
display_order INTEGER NOT NULL DEFAULT 0,
|
||||
synced_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
`);
|
||||
await query("ALTER TABLE project_teamspace_subentries ADD COLUMN IF NOT EXISTS user_id BIGINT REFERENCES users(id) ON DELETE CASCADE;");
|
||||
await query(`
|
||||
UPDATE project_teamspace_subentries pts
|
||||
SET user_id = p.user_id
|
||||
FROM project_teamspace_entries pte
|
||||
JOIN projects p ON p.id = pte.project_id
|
||||
WHERE pte.id = pts.project_teamspace_entry_id
|
||||
AND pts.user_id IS NULL
|
||||
AND p.user_id IS NOT NULL;
|
||||
`);
|
||||
await query(`
|
||||
DO $$
|
||||
DECLARE
|
||||
constraint_name text;
|
||||
BEGIN
|
||||
SELECT c.conname
|
||||
INTO constraint_name
|
||||
FROM pg_constraint c
|
||||
JOIN pg_class t ON t.oid = c.conrelid
|
||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||
WHERE n.nspname = current_schema()
|
||||
AND t.relname = 'project_teamspace_subentries'
|
||||
AND c.contype = 'u'
|
||||
AND pg_get_constraintdef(c.oid) LIKE '%project_teamspace_entry_id%'
|
||||
AND pg_get_constraintdef(c.oid) LIKE '%teamspace_api_project_id%'
|
||||
AND pg_get_constraintdef(c.oid) NOT LIKE '%user_id%'
|
||||
LIMIT 1;
|
||||
|
||||
IF constraint_name IS NOT NULL THEN
|
||||
EXECUTE format('ALTER TABLE project_teamspace_subentries DROP CONSTRAINT %I', constraint_name);
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
await query("ALTER TABLE project_teamspace_subentries ADD COLUMN IF NOT EXISTS alias TEXT NOT NULL DEFAULT '';");
|
||||
await query("ALTER TABLE project_teamspace_subentries ADD COLUMN IF NOT EXISTS description TEXT NOT NULL DEFAULT '';");
|
||||
await query("ALTER TABLE project_teamspace_subentries ADD COLUMN IF NOT EXISTS billing_type TEXT;");
|
||||
await query("ALTER TABLE project_teamspace_subentries ADD COLUMN IF NOT EXISTS planned_income NUMERIC;");
|
||||
await query("ALTER TABLE project_teamspace_subentries ADD COLUMN IF NOT EXISTS sum_income NUMERIC;");
|
||||
await query("ALTER TABLE project_teamspace_subentries DROP CONSTRAINT IF EXISTS project_teamspace_subentries_hour_budget_nonnegative;");
|
||||
await query("ALTER TABLE project_teamspace_subentries DROP CONSTRAINT IF EXISTS project_teamspace_subentries_manual_consumption_nonnegative;");
|
||||
await query("ALTER TABLE project_teamspace_subentries DROP CONSTRAINT IF EXISTS project_teamspace_subentries_manual_unit_quantity_nonnegative;");
|
||||
await query("ALTER TABLE project_teamspace_subentries DROP COLUMN IF EXISTS hour_budget_minutes;");
|
||||
await query("ALTER TABLE project_teamspace_subentries DROP COLUMN IF EXISTS manual_consumption_minutes;");
|
||||
await query("ALTER TABLE project_teamspace_subentries DROP COLUMN IF EXISTS manual_unit_quantity;");
|
||||
await query("ALTER TABLE project_teamspace_subentries DROP COLUMN IF EXISTS is_fulfilled;");
|
||||
await query("CREATE INDEX IF NOT EXISTS idx_project_teamspace_subentries_entry_order ON project_teamspace_subentries(project_teamspace_entry_id, display_order, id);");
|
||||
await query("CREATE INDEX IF NOT EXISTS idx_project_teamspace_subentries_user_entry_order ON project_teamspace_subentries(user_id, project_teamspace_entry_id, display_order, id);");
|
||||
await query("CREATE UNIQUE INDEX IF NOT EXISTS idx_project_teamspace_subentries_user_entry_api ON project_teamspace_subentries(user_id, project_teamspace_entry_id, teamspace_api_project_id);");
|
||||
await query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
@@ -843,15 +952,18 @@ export async function migrate() {
|
||||
DROP CONSTRAINT ticket_day_billings_project_teamspace_entry_fkey;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'ticket_day_billings_project_teamspace_entry_fkey'
|
||||
) THEN
|
||||
ALTER TABLE ticket_day_billings
|
||||
ADD CONSTRAINT ticket_day_billings_project_teamspace_entry_fkey
|
||||
FOREIGN KEY (project_teamspace_entry_id) REFERENCES project_teamspace_entries(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'ticket_day_billings_project_teamspace_entry_fkey'
|
||||
) THEN
|
||||
ALTER TABLE ticket_day_billings
|
||||
ADD CONSTRAINT ticket_day_billings_project_teamspace_entry_fkey
|
||||
FOREIGN KEY (project_teamspace_entry_id) REFERENCES project_teamspace_entries(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
|
||||
ALTER TABLE ticket_day_billings
|
||||
DROP CONSTRAINT IF EXISTS ticket_day_billings_project_teamspace_subentry_fkey;
|
||||
END $$;
|
||||
`);
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS hidden_user_projects (
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
@@ -873,6 +985,78 @@ export async function migrate() {
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
await query("ALTER TABLE sessions DROP CONSTRAINT IF EXISTS sessions_project_teamspace_subentry_fkey;");
|
||||
await query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'sessions'
|
||||
AND column_name = 'project_teamspace_subentry_id'
|
||||
) THEN
|
||||
UPDATE sessions s
|
||||
SET project_teamspace_entry_id = COALESCE(s.project_teamspace_entry_id, pts.project_teamspace_entry_id)
|
||||
FROM project_teamspace_subentries pts
|
||||
WHERE s.project_teamspace_subentry_id = pts.id;
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
await query(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'ticket_day_billings'
|
||||
AND column_name = 'project_teamspace_subentry_id'
|
||||
) THEN
|
||||
CREATE TEMP TABLE migrated_subentry_day_billings ON COMMIT DROP AS
|
||||
SELECT
|
||||
tdb.ticket_id,
|
||||
tdb.user_id,
|
||||
tdb.day,
|
||||
pts.project_teamspace_entry_id,
|
||||
SUM(tdb.billed_minutes)::int AS billed_minutes
|
||||
FROM ticket_day_billings tdb
|
||||
JOIN project_teamspace_subentries pts ON pts.id = tdb.project_teamspace_subentry_id
|
||||
GROUP BY tdb.ticket_id, tdb.user_id, tdb.day, pts.project_teamspace_entry_id;
|
||||
|
||||
DELETE FROM ticket_day_billings tdb
|
||||
WHERE tdb.project_teamspace_subentry_id IS NOT NULL;
|
||||
|
||||
UPDATE ticket_day_billings target
|
||||
SET billed_minutes = target.billed_minutes + source.billed_minutes,
|
||||
updated_at = now()
|
||||
FROM migrated_subentry_day_billings source
|
||||
WHERE target.ticket_id = source.ticket_id
|
||||
AND target.user_id = source.user_id
|
||||
AND target.day = source.day
|
||||
AND target.project_teamspace_entry_id = source.project_teamspace_entry_id;
|
||||
|
||||
INSERT INTO ticket_day_billings (ticket_id, user_id, project_teamspace_entry_id, day, billed_minutes)
|
||||
SELECT source.ticket_id, source.user_id, source.project_teamspace_entry_id, source.day, source.billed_minutes
|
||||
FROM migrated_subentry_day_billings source
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ticket_day_billings target
|
||||
WHERE target.ticket_id = source.ticket_id
|
||||
AND target.user_id = source.user_id
|
||||
AND target.day = source.day
|
||||
AND target.project_teamspace_entry_id = source.project_teamspace_entry_id
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
await query("DROP INDEX IF EXISTS idx_sessions_project_teamspace_subentry;");
|
||||
await query("DROP INDEX IF EXISTS idx_ticket_day_billings_teamspace_subentry;");
|
||||
await query("DROP INDEX IF EXISTS idx_ticket_day_billings_ticket_user_day_subentry;");
|
||||
await query("ALTER TABLE sessions DROP COLUMN IF EXISTS project_teamspace_subentry_id;");
|
||||
await query("ALTER TABLE ticket_day_billings DROP COLUMN IF EXISTS project_teamspace_subentry_id;");
|
||||
await query("CREATE UNIQUE INDEX IF NOT EXISTS idx_ticket_day_billings_ticket_user_day_default ON ticket_day_billings(ticket_id, user_id, day) WHERE project_teamspace_entry_id IS NULL;");
|
||||
await query("CREATE UNIQUE INDEX IF NOT EXISTS idx_ticket_day_billings_ticket_user_day_entry ON ticket_day_billings(ticket_id, user_id, day, project_teamspace_entry_id) WHERE project_teamspace_entry_id IS NOT NULL;");
|
||||
|
||||
await query(`
|
||||
UPDATE tickets t
|
||||
@@ -1025,8 +1209,8 @@ export async function migrate() {
|
||||
await query("CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_recurring_occurrence_key ON sessions(recurring_occurrence_key) WHERE recurring_occurrence_key IS NOT NULL;");
|
||||
await query("CREATE INDEX IF NOT EXISTS idx_sessions_billing_status ON sessions(billing_status);");
|
||||
await query("CREATE INDEX IF NOT EXISTS idx_ticket_day_closures_day ON ticket_day_closures(day);");
|
||||
await query("CREATE INDEX IF NOT EXISTS idx_ticket_day_billings_user_day ON ticket_day_billings(user_id, day);");
|
||||
await query("CREATE INDEX IF NOT EXISTS idx_ticket_day_billings_teamspace_entry ON ticket_day_billings(project_teamspace_entry_id);");
|
||||
await query("CREATE INDEX IF NOT EXISTS idx_ticket_day_billings_user_day ON ticket_day_billings(user_id, day);");
|
||||
await query("CREATE INDEX IF NOT EXISTS idx_ticket_day_billings_teamspace_entry ON ticket_day_billings(project_teamspace_entry_id);");
|
||||
await query("CREATE INDEX IF NOT EXISTS idx_ticket_day_billing_ack_user_day ON ticket_day_billing_acknowledgements(user_id, day);");
|
||||
await query("CREATE INDEX IF NOT EXISTS idx_projects_user_ticket ON projects(user_id, ticket_id);");
|
||||
await query("CREATE INDEX IF NOT EXISTS idx_projects_user_updated ON projects(user_id, updated_at DESC);");
|
||||
|
||||
Reference in new issue
Block a user