Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions admin-dashboard/migrations/db_init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ create table if not exists public.tasks (
status text not null default 'backlog',
priority text not null default 'medium',
label text not null default 'feature',
due_date timestamptz,
due_date date,
assignee_id uuid,
created_by uuid not null,
created_at timestamptz not null default now(),
Expand Down Expand Up @@ -278,21 +278,27 @@ create policy "profiles_update_self"
-- POLICIES — workspaces
-- ============================================================================

-- Owners are included explicitly so that INSERT ... RETURNING (used by
-- PostgREST when the client chains .select()) can see the freshly-created row
-- before any workspace_members entry exists.
create policy "workspaces_select_member"
on public.workspaces for select
to authenticated
using (public.is_workspace_member(id));
using (owner_id = auth.uid() or public.is_workspace_member(id));
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

create policy "workspaces_insert_self_as_owner"
on public.workspaces for insert
to authenticated
with check (owner_id = auth.uid());

-- WITH CHECK pins owner_id to the caller so the owner cannot transfer ownership
-- by updating workspaces alone — otherwise the SELECT policy's owner_id branch
-- would grant the new owner_id read access without a workspace_members row.
create policy "workspaces_update_owner"
on public.workspaces for update
to authenticated
using (public.is_workspace_owner(id))
with check (public.is_workspace_owner(id));
with check (public.is_workspace_owner(id) and owner_id = auth.uid());

create policy "workspaces_delete_owner"
on public.workspaces for delete
Expand Down
2 changes: 0 additions & 2 deletions admin-dashboard/src/components/layout/sidebar-nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
LayoutDashboard,
ListTodo,
Users,
Boxes,
MessageSquare,
Settings,
LifeBuoy,
Expand All @@ -19,7 +18,6 @@ export const NAV_ITEMS: NavItem[] = [
{ title: 'Dashboard', to: '/dashboard', icon: LayoutDashboard },
{ title: 'Tasks', to: '/tasks', icon: ListTodo },
{ title: 'Users', to: '/users', icon: Users },
{ title: 'Apps', to: '/apps', icon: Boxes },
{ title: 'Chats', to: '/chats', icon: MessageSquare },
{ title: 'Settings', to: '/settings', icon: Settings },
{ title: 'Help', to: '/help-center', icon: LifeBuoy },
Expand Down
15 changes: 11 additions & 4 deletions admin-dashboard/src/features/tasks/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,16 @@ export const labelOptions = (Object.keys(LABEL_META) as TaskLabel[]).map((value)
icon: LABEL_META[value].icon,
}))

// due_date is a plain calendar date (no time / no timezone). Parse it as a
// local date so MMM d formatting doesn't shift by a day in negative UTC offsets.
function parseLocalDate(value: string) {
const [y, m, d] = value.split('-').map(Number)
return new Date(y, m - 1, d)
}

function formatDueDate(value: string | null) {
if (!value) return null
const d = new Date(value)
const d = parseLocalDate(value)
if (Number.isNaN(d.getTime())) return null
if (isToday(d)) return 'Today'
if (isTomorrow(d)) return 'Tomorrow'
Expand Down Expand Up @@ -242,15 +249,15 @@ export function buildColumns(actions: TaskRowActions): ColumnDef<Task>[] {
cell: ({ row }) => {
const text = formatDueDate(row.original.due_date)
if (!text) return <span className="text-muted-foreground">—</span>
const d = new Date(row.original.due_date!)
const d = parseLocalDate(row.original.due_date!)
const overdue = d.getTime() < Date.now() && row.original.status !== 'done'
return (
<span className={cn('text-sm', overdue && 'text-destructive')}>{text}</span>
)
},
sortingFn: (a, b) => {
const av = a.original.due_date ? new Date(a.original.due_date).getTime() : Number.POSITIVE_INFINITY
const bv = b.original.due_date ? new Date(b.original.due_date).getTime() : Number.POSITIVE_INFINITY
const av = a.original.due_date ? parseLocalDate(a.original.due_date).getTime() : Number.POSITIVE_INFINITY
const bv = b.original.due_date ? parseLocalDate(b.original.due_date).getTime() : Number.POSITIVE_INFINITY
return av - bv
},
},
Expand Down
3 changes: 1 addition & 2 deletions admin-dashboard/src/features/tasks/task-form-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useEffect } from 'react'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { format, parseISO } from 'date-fns'
import { Button } from '@/components/ui/button'
import {
Dialog,
Expand Down Expand Up @@ -58,7 +57,7 @@ function toFormValues(task: Task | null | undefined, fallback?: Partial<TaskForm
status: task.status,
priority: task.priority,
label: task.label,
due_date: task.due_date ? format(parseISO(task.due_date), 'yyyy-MM-dd') : '',
due_date: task.due_date ?? '',
}
}
return { ...defaultTaskFormValues, ...fallback }
Expand Down
2 changes: 1 addition & 1 deletion admin-dashboard/src/features/tasks/use-tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function toDbPayload(values: TaskFormValues) {
status: values.status,
priority: values.priority,
label: values.label,
due_date: values.due_date ? new Date(values.due_date).toISOString() : null,
due_date: values.due_date ? values.due_date : null,
}
}

Expand Down
3 changes: 0 additions & 3 deletions admin-dashboard/src/routes/(auth)/sign-in-2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ function SignIn2Page() {
toast.error(error?.message ?? `${provider} sign-in is not configured`)
return
}
if (data.codeVerifier) {
sessionStorage.setItem('insforge.pkce_verifier', data.codeVerifier)
}
window.location.href = data.url
}

Expand Down
3 changes: 0 additions & 3 deletions admin-dashboard/src/routes/(auth)/sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,6 @@ function SignInPage() {
toast.error(error?.message ?? `${provider} sign-in is not configured`)
return
}
if (data.codeVerifier) {
sessionStorage.setItem('insforge.pkce_verifier', data.codeVerifier)
}
window.location.href = data.url
}

Expand Down
24 changes: 4 additions & 20 deletions admin-dashboard/src/routes/auth/callback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { useAuth } from '@/lib/auth-context'
import { ensureWorkspace } from '@/features/auth/ensure-workspace'
import { useWorkspaceStore } from '@/features/workspaces/workspace-store'

const VERIFIER_KEY = 'insforge.pkce_verifier'

export const Route = createFileRoute('/auth/callback')({
component: OAuthCallbackPage,
})
Expand All @@ -24,30 +22,17 @@ function OAuthCallbackPage() {
ran.current = true

void (async () => {
// Read directly from window.location so the SDK's exact param names work
// regardless of how TanStack Router parses search.
const params = new URLSearchParams(window.location.search)
const code = params.get('insforge_code') ?? params.get('code')
const err = params.get('error')
const errDesc = params.get('error_description')

if (err) {
toast.error(errDesc ?? err)
sessionStorage.removeItem(VERIFIER_KEY)
toast.error(params.get('error_description') ?? err)
navigate({ to: '/sign-in' })
return
}

if (!code) {
toast.error('Missing authorization code')
navigate({ to: '/sign-in' })
return
}

const codeVerifier = sessionStorage.getItem(VERIFIER_KEY) ?? undefined
sessionStorage.removeItem(VERIFIER_KEY)

const { data, error } = await insforge.auth.exchangeOAuthCode(code, codeVerifier)
// The SDK auto-detects `insforge_code` on init and exchanges it; getCurrentUser
// waits for that pending exchange to settle before returning.
const { data, error } = await insforge.auth.getCurrentUser()
if (error || !data?.user) {
toast.error(error?.message ?? 'Sign-in failed')
navigate({ to: '/sign-in' })
Expand All @@ -65,7 +50,6 @@ function OAuthCallbackPage() {
const msg = err instanceof Error ? err.message : 'Could not initialize workspace'
console.error('ensureWorkspace failed:', err)
toast.error(`Workspace setup failed: ${msg}`, { duration: 10000 })
// Surface, but still sign the user in so they can retry from the UI.
await refresh()
navigate({ to: '/dashboard' })
}
Expand Down
Loading