Update
This commit is contained in:
1 parent
2105a0c5f7
commit
c89d4825e2
30 files changed
+5435
-627
No files matched your search
+4
-1
@@ -22,6 +22,7 @@ export type AuthUser = {
|
||||
zammad_session_note_default_enabled: boolean;
|
||||
zammad_time_unit_rounding_enabled: boolean;
|
||||
zammad_time_unit_rounding_minutes: number;
|
||||
zammad_atoss_email_sync_enabled: boolean;
|
||||
zammad_article_type_id: number;
|
||||
};
|
||||
|
||||
@@ -89,6 +90,7 @@ function publicUser(user: AuthUser) {
|
||||
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),
|
||||
zammad_atoss_email_sync_enabled: Boolean(user.zammad_atoss_email_sync_enabled ?? true),
|
||||
zammad_article_type_id: Number(user.zammad_article_type_id ?? 6)
|
||||
};
|
||||
}
|
||||
@@ -152,7 +154,8 @@ export async function requireAuth(req: Request, res: Response, next: NextFunctio
|
||||
u.zammad_session_note_default_enabled,
|
||||
u.zammad_time_unit_rounding_enabled,
|
||||
u.zammad_time_unit_rounding_minutes,
|
||||
u.zammad_article_type_id
|
||||
u.zammad_atoss_email_sync_enabled,
|
||||
COALESCE(u.zammad_article_type_id, 6) AS zammad_article_type_id
|
||||
FROM auth_sessions s
|
||||
JOIN users u ON u.id = s.user_id
|
||||
WHERE s.token_hash = $1
|
||||
|
||||
@@ -22,6 +22,7 @@ export const databaseExportTables = [
|
||||
"recurring_billing_slots",
|
||||
"recurring_billing_exceptions",
|
||||
"sessions",
|
||||
"active_timers",
|
||||
"ticket_month_closures",
|
||||
"month_closures",
|
||||
"ticket_day_closures",
|
||||
|
||||
+2382
-223
File diff suppressed because it is too large.
Load diff
+130
-6
@@ -91,8 +91,11 @@ export async function migrate() {
|
||||
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;");
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS zammad_atoss_email_sync_enabled BOOLEAN NOT NULL DEFAULT true;");
|
||||
await query("ALTER TABLE users ADD COLUMN IF NOT EXISTS zammad_article_type_id INTEGER DEFAULT 6;");
|
||||
await query("ALTER TABLE users ALTER COLUMN zammad_article_type_id DROP NOT NULL;");
|
||||
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;");
|
||||
await query(`
|
||||
ALTER TABLE users
|
||||
DROP CONSTRAINT IF EXISTS users_zammad_time_unit_rounding_minutes_valid,
|
||||
@@ -101,7 +104,7 @@ export async function migrate() {
|
||||
await query(`
|
||||
ALTER TABLE users
|
||||
DROP CONSTRAINT IF EXISTS users_zammad_article_type_id_valid,
|
||||
ADD CONSTRAINT users_zammad_article_type_id_valid CHECK (zammad_article_type_id IS NULL OR zammad_article_type_id IN (1, 2, 3, 4, 5, 6));
|
||||
ADD CONSTRAINT users_zammad_article_type_id_valid CHECK (zammad_article_type_id IN (1, 2, 3, 4, 5, 6));
|
||||
`);
|
||||
|
||||
await query(`
|
||||
@@ -237,6 +240,86 @@ export async function migrate() {
|
||||
await query("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS recurring_occurrence_date DATE;");
|
||||
await query("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS recurring_occurrence_key TEXT;");
|
||||
await query("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS project_teamspace_entry_id BIGINT;");
|
||||
await query("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS activity_html TEXT;");
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS active_timers (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
ticket_number TEXT NOT NULL,
|
||||
organization_name TEXT,
|
||||
work_type TEXT,
|
||||
is_project BOOLEAN NOT NULL DEFAULT false,
|
||||
project_name TEXT,
|
||||
ticket_lookup_done BOOLEAN NOT NULL DEFAULT false,
|
||||
activity TEXT NOT NULL DEFAULT '',
|
||||
activity_html TEXT,
|
||||
billing_status TEXT,
|
||||
started_at TIMESTAMPTZ NOT NULL,
|
||||
paused_total_ms BIGINT NOT NULL DEFAULT 0,
|
||||
paused_at TIMESTAMPTZ,
|
||||
phase TEXT NOT NULL DEFAULT 'running',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT active_timers_ticket_number_format CHECK (ticket_number ~ '^Ticket#[0-9]{6}$'),
|
||||
CONSTRAINT active_timers_work_type_valid CHECK (work_type IS NULL OR work_type IN ('support', 'consulting')),
|
||||
CONSTRAINT active_timers_billing_status_valid CHECK (billing_status IS NULL OR billing_status IN ('billable', 'billed', 'non_billable')),
|
||||
CONSTRAINT active_timers_phase_valid CHECK (phase IN ('running', 'paused')),
|
||||
CONSTRAINT active_timers_paused_total_non_negative CHECK (paused_total_ms >= 0),
|
||||
CONSTRAINT active_timers_paused_state_valid CHECK (
|
||||
(phase = 'running' AND paused_at IS NULL)
|
||||
OR (phase = 'paused' AND paused_at IS NOT NULL)
|
||||
)
|
||||
);
|
||||
`);
|
||||
await query("ALTER TABLE active_timers ADD COLUMN IF NOT EXISTS organization_name TEXT;");
|
||||
await query("ALTER TABLE active_timers ADD COLUMN IF NOT EXISTS work_type TEXT;");
|
||||
await query("ALTER TABLE active_timers ADD COLUMN IF NOT EXISTS is_project BOOLEAN NOT NULL DEFAULT false;");
|
||||
await query("ALTER TABLE active_timers ADD COLUMN IF NOT EXISTS project_name TEXT;");
|
||||
await query("ALTER TABLE active_timers ADD COLUMN IF NOT EXISTS ticket_lookup_done BOOLEAN NOT NULL DEFAULT false;");
|
||||
await query("ALTER TABLE active_timers ADD COLUMN IF NOT EXISTS activity TEXT NOT NULL DEFAULT '';");
|
||||
await query("ALTER TABLE active_timers ADD COLUMN IF NOT EXISTS activity_html TEXT;");
|
||||
await query("ALTER TABLE active_timers ADD COLUMN IF NOT EXISTS billing_status TEXT;");
|
||||
await query("ALTER TABLE active_timers ADD COLUMN IF NOT EXISTS paused_total_ms BIGINT NOT NULL DEFAULT 0;");
|
||||
await query("ALTER TABLE active_timers ADD COLUMN IF NOT EXISTS paused_at TIMESTAMPTZ;");
|
||||
await query("ALTER TABLE active_timers ADD COLUMN IF NOT EXISTS phase TEXT NOT NULL DEFAULT 'running';");
|
||||
await query("ALTER TABLE active_timers ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT now();");
|
||||
await query("ALTER TABLE active_timers ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT now();");
|
||||
await query(`
|
||||
ALTER TABLE active_timers
|
||||
DROP CONSTRAINT IF EXISTS active_timers_ticket_number_format,
|
||||
ADD CONSTRAINT active_timers_ticket_number_format CHECK (ticket_number ~ '^Ticket#[0-9]{6}$');
|
||||
`);
|
||||
await query(`
|
||||
ALTER TABLE active_timers
|
||||
DROP CONSTRAINT IF EXISTS active_timers_work_type_valid,
|
||||
ADD CONSTRAINT active_timers_work_type_valid CHECK (work_type IS NULL OR work_type IN ('support', 'consulting'));
|
||||
`);
|
||||
await query(`
|
||||
ALTER TABLE active_timers
|
||||
DROP CONSTRAINT IF EXISTS active_timers_billing_status_valid,
|
||||
ADD CONSTRAINT active_timers_billing_status_valid CHECK (billing_status IS NULL OR billing_status IN ('billable', 'billed', 'non_billable'));
|
||||
`);
|
||||
await query(`
|
||||
ALTER TABLE active_timers
|
||||
DROP CONSTRAINT IF EXISTS active_timers_phase_valid,
|
||||
ADD CONSTRAINT active_timers_phase_valid CHECK (phase IN ('running', 'paused'));
|
||||
`);
|
||||
await query(`
|
||||
ALTER TABLE active_timers
|
||||
DROP CONSTRAINT IF EXISTS active_timers_paused_total_non_negative,
|
||||
ADD CONSTRAINT active_timers_paused_total_non_negative CHECK (paused_total_ms >= 0);
|
||||
`);
|
||||
await query(`
|
||||
ALTER TABLE active_timers
|
||||
DROP CONSTRAINT IF EXISTS active_timers_paused_state_valid,
|
||||
ADD CONSTRAINT active_timers_paused_state_valid CHECK (
|
||||
(phase = 'running' AND paused_at IS NULL)
|
||||
OR (phase = 'paused' AND paused_at IS NOT NULL)
|
||||
);
|
||||
`);
|
||||
await query("CREATE UNIQUE INDEX IF NOT EXISTS idx_active_timers_user_ticket ON active_timers(user_id, ticket_number);");
|
||||
await query("CREATE UNIQUE INDEX IF NOT EXISTS idx_active_timers_single_running ON active_timers(user_id) WHERE phase = 'running';");
|
||||
await query("CREATE INDEX IF NOT EXISTS idx_active_timers_user_updated ON active_timers(user_id, updated_at);");
|
||||
await query(`
|
||||
ALTER TABLE sessions
|
||||
DROP CONSTRAINT IF EXISTS billing_status_valid,
|
||||
@@ -263,6 +346,7 @@ export async function migrate() {
|
||||
ON zammad_email_session_events(delivery_id)
|
||||
WHERE delivery_id IS NOT NULL;
|
||||
`);
|
||||
await query("CREATE INDEX IF NOT EXISTS idx_zammad_email_session_events_session ON zammad_email_session_events(session_id);");
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS zammad_session_article_queue (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
@@ -272,8 +356,10 @@ export async function migrate() {
|
||||
ticket_number TEXT NOT NULL,
|
||||
zammad_ticket_id BIGINT,
|
||||
activity TEXT NOT NULL,
|
||||
article_body TEXT,
|
||||
article_content_type TEXT NOT NULL DEFAULT 'text/plain',
|
||||
time_unit NUMERIC(10, 2) NOT NULL,
|
||||
zammad_article_type_id INTEGER DEFAULT 6,
|
||||
zammad_article_type_id INTEGER NOT NULL DEFAULT 6,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
attempt_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_error TEXT,
|
||||
@@ -285,12 +371,22 @@ export async function migrate() {
|
||||
CONSTRAINT zammad_session_article_queue_status_valid CHECK (status IN ('pending', 'sent'))
|
||||
);
|
||||
`);
|
||||
await query("ALTER TABLE zammad_session_article_queue ADD COLUMN IF NOT EXISTS article_body TEXT;");
|
||||
await query("ALTER TABLE zammad_session_article_queue ADD COLUMN IF NOT EXISTS article_content_type TEXT NOT NULL DEFAULT 'text/plain';");
|
||||
await query("UPDATE zammad_session_article_queue SET article_body = activity WHERE article_body IS NULL;");
|
||||
await query(`
|
||||
ALTER TABLE zammad_session_article_queue
|
||||
DROP CONSTRAINT IF EXISTS zammad_session_article_queue_content_type_valid,
|
||||
ADD CONSTRAINT zammad_session_article_queue_content_type_valid CHECK (article_content_type IN ('text/plain', 'text/html'));
|
||||
`);
|
||||
await query("ALTER TABLE zammad_session_article_queue ADD COLUMN IF NOT EXISTS zammad_article_type_id INTEGER DEFAULT 6;");
|
||||
await query("ALTER TABLE zammad_session_article_queue ALTER COLUMN zammad_article_type_id DROP NOT NULL;");
|
||||
await query("UPDATE zammad_session_article_queue SET zammad_article_type_id = 6 WHERE zammad_article_type_id IS NULL;");
|
||||
await query("ALTER TABLE zammad_session_article_queue ALTER COLUMN zammad_article_type_id SET DEFAULT 6;");
|
||||
await query("ALTER TABLE zammad_session_article_queue ALTER COLUMN zammad_article_type_id SET NOT NULL;");
|
||||
await query(`
|
||||
ALTER TABLE zammad_session_article_queue
|
||||
DROP CONSTRAINT IF EXISTS zammad_session_article_queue_type_id_valid,
|
||||
ADD CONSTRAINT zammad_session_article_queue_type_id_valid CHECK (zammad_article_type_id IS NULL OR zammad_article_type_id IN (1, 2, 3, 4, 5, 6));
|
||||
ADD CONSTRAINT zammad_session_article_queue_type_id_valid CHECK (zammad_article_type_id IN (1, 2, 3, 4, 5, 6));
|
||||
`);
|
||||
await query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_zammad_session_article_queue_pending_session
|
||||
@@ -546,14 +642,18 @@ export async function migrate() {
|
||||
CREATE TABLE IF NOT EXISTS ticket_day_billings (
|
||||
ticket_id BIGINT NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
project_teamspace_entry_id BIGINT,
|
||||
day DATE NOT NULL,
|
||||
billed_minutes INTEGER NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (ticket_id, user_id, day),
|
||||
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(`
|
||||
CREATE TABLE IF NOT EXISTS ticket_day_billing_acknowledgements (
|
||||
@@ -610,6 +710,7 @@ export async function migrate() {
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
project_id BIGINT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
teamspace_project TEXT,
|
||||
teamspace_description TEXT NOT NULL DEFAULT '',
|
||||
is_default BOOLEAN NOT NULL DEFAULT false,
|
||||
hour_budget_minutes INTEGER,
|
||||
budget_sync_disabled BOOLEAN NOT NULL DEFAULT false,
|
||||
@@ -622,9 +723,31 @@ 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("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(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'ticket_day_billings_project_teamspace_entry_fkey'
|
||||
AND confdeltype <> 'n'
|
||||
) THEN
|
||||
ALTER TABLE ticket_day_billings
|
||||
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 $$;
|
||||
`);
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS hidden_user_projects (
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
@@ -786,6 +909,7 @@ export async function migrate() {
|
||||
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_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);");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const ticketPattern = /^Ticket#\d{6}$/;
|
||||
export const trackableTicketPattern = /^(Ticket#\d{6}|Fix#\d+)$/;
|
||||
export const monthPattern = /^\d{4}-\d{2}$/;
|
||||
export const weekPattern = /^\d{4}-W\d{2}$/;
|
||||
export const dayPattern = /^\d{4}-\d{2}-\d{2}$/;
|
||||
export const usernamePattern = /^\S{3,40}$/u;
|
||||
|
||||
@@ -87,6 +88,41 @@ export function parseDay(value: string) {
|
||||
};
|
||||
}
|
||||
|
||||
export function parseWeek(value: string) {
|
||||
if (!weekPattern.test(value)) {
|
||||
throw badRequest("week must use YYYY-Www");
|
||||
}
|
||||
|
||||
const year = Number(value.slice(0, 4));
|
||||
const week = Number(value.slice(6, 8));
|
||||
|
||||
if (!Number.isInteger(year) || !Number.isInteger(week) || week < 1 || week > 53) {
|
||||
throw badRequest("week is invalid");
|
||||
}
|
||||
|
||||
const jan4 = new Date(Date.UTC(year, 0, 4));
|
||||
const jan4Weekday = jan4.getUTCDay() || 7;
|
||||
const weekStart = new Date(jan4);
|
||||
weekStart.setUTCDate(jan4.getUTCDate() - jan4Weekday + 1 + (week - 1) * 7);
|
||||
|
||||
const checkThursday = new Date(weekStart);
|
||||
checkThursday.setUTCDate(weekStart.getUTCDate() + 3);
|
||||
|
||||
if (checkThursday.getUTCFullYear() !== year) {
|
||||
throw badRequest("week is invalid");
|
||||
}
|
||||
|
||||
const next = new Date(weekStart);
|
||||
next.setUTCDate(weekStart.getUTCDate() + 7);
|
||||
|
||||
return {
|
||||
label: value,
|
||||
start: weekStart.toISOString().slice(0, 10),
|
||||
startIso: weekStart.toISOString(),
|
||||
endIso: next.toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
export function parsePositiveInteger(value: unknown, field: string) {
|
||||
const number = Number(value);
|
||||
|
||||
|
||||
Reference in new issue
Block a user