Remove scheduler connections and meeting extras
This commit is contained in:
@@ -1,12 +0,0 @@
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
|
||||
import { requireSchedulerSession } from "@scheduler/lib/scheduler/auth";
|
||||
import { getConnections } from "@scheduler/lib/scheduler/service";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const auth = await requireSchedulerSession(request);
|
||||
if ("response" in auth) return auth.response;
|
||||
return NextResponse.json(await getConnections(auth.userId));
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { AppShell } from "@scheduler/components/AppShell";
|
||||
import { ConnectionsView } from "@scheduler/components/ConnectionsView";
|
||||
import { getServerViewerId } from "@scheduler/lib/scheduler/auth";
|
||||
import { getConnections, getTeamContext } from "@scheduler/lib/scheduler/service";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function ConnectionsPage() {
|
||||
const viewerId = await getServerViewerId();
|
||||
if (viewerId === null) {
|
||||
redirect("/login?callbackUrl=/connections");
|
||||
}
|
||||
|
||||
const [context, { connections }] = await Promise.all([getTeamContext(viewerId), getConnections(viewerId)]);
|
||||
|
||||
return (
|
||||
<AppShell active="connections" user={{ name: context.viewer.name, timeZone: context.viewer.timeZone }}>
|
||||
<ConnectionsView connections={connections} />
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
@@ -725,8 +725,7 @@ input[type="number"]::-webkit-inner-spin-button { filter: invert(0.5); }
|
||||
AVAILABILITY + SETTINGS
|
||||
================================================================ */
|
||||
|
||||
.availability-layout,
|
||||
.connections-layout {
|
||||
.availability-layout {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
max-width: 600px;
|
||||
@@ -886,54 +885,6 @@ input[type="number"]::-webkit-inner-spin-button { filter: invert(0.5); }
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
CONNECTIONS
|
||||
================================================================ */
|
||||
|
||||
.provider-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.provider-status.connected {
|
||||
font-size: 10px;
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
font-family: var(--font-mono);
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
background: var(--accent-bg);
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--accent-border);
|
||||
}
|
||||
|
||||
.browse-button {
|
||||
display: inline-flex;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.privacy-note {
|
||||
font-size: 10.5px;
|
||||
color: var(--text-3);
|
||||
font-family: var(--font-mono);
|
||||
letter-spacing: 0.04em;
|
||||
padding: 14px 0 2px;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
RESPONSIVE
|
||||
================================================================ */
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import Link from "next/link";
|
||||
|
||||
type ActiveNavItem = "calendar" | "availability" | "connections";
|
||||
type ActiveNavItem = "calendar" | "availability";
|
||||
type NavItem = { id: ActiveNavItem; href: string; label: string };
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ id: "calendar", href: "/calendar", label: "Calendar" },
|
||||
{ id: "availability", href: "/availability", label: "Availability" },
|
||||
{ id: "connections", href: "/connections", label: "Connections" },
|
||||
];
|
||||
|
||||
export function AppShell({
|
||||
|
||||
@@ -92,8 +92,6 @@ export function CalendarWorkspace({
|
||||
const [slots, setSlots] = useState<MutualSlot[]>(initialSlots);
|
||||
const [durationMinutes, setDurationMinutes] = useState<number>(30);
|
||||
const [title, setTitle] = useState("");
|
||||
const [location, setLocation] = useState("");
|
||||
const [conferencing, setConferencing] = useState(false);
|
||||
const [selectedSlotStart, setSelectedSlotStart] = useState<string | null>(null);
|
||||
const [createState, setCreateState] = useState<CreateState>({ kind: "idle" });
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -167,8 +165,6 @@ export function CalendarWorkspace({
|
||||
attendeeIds: selectedAttendeeIds,
|
||||
start: selectedSlotStart,
|
||||
durationMinutes,
|
||||
location: location.trim() || undefined,
|
||||
conferencing: conferencing ? "provider" : "none",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -188,7 +184,7 @@ export function CalendarWorkspace({
|
||||
} catch {
|
||||
setCreateState({ kind: "error", message: "Could not create the meeting." });
|
||||
}
|
||||
}, [selectedSlotStart, title, selectedAttendeeIds, durationMinutes, location, conferencing]);
|
||||
}, [selectedSlotStart, title, selectedAttendeeIds, durationMinutes]);
|
||||
|
||||
return (
|
||||
<div className="calendar-layout">
|
||||
@@ -304,16 +300,6 @@ export function CalendarWorkspace({
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<label className="field">
|
||||
<span>Location (optional)</span>
|
||||
<input value={location} onChange={(event) => setLocation(event.target.value)} placeholder="Room or address" />
|
||||
</label>
|
||||
|
||||
<label className="field-inline">
|
||||
<input type="checkbox" checked={conferencing} onChange={(event) => setConferencing(event.target.checked)} />
|
||||
<span>Add conferencing link (optional)</span>
|
||||
</label>
|
||||
|
||||
<div className="selected-slot">
|
||||
{selectedSlotStart ? (
|
||||
<span>
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import type { SchedulerConnection } from "@scheduler/lib/scheduler/service";
|
||||
|
||||
type ConnectionsViewProps = {
|
||||
connections: SchedulerConnection[];
|
||||
};
|
||||
|
||||
const CONFERENCING_CATEGORIES = new Set(["conferencing", "video"]);
|
||||
const CAL_SETTINGS_URL = process.env.CAL_SETTINGS_URL ?? "https://scheduler.internal.vyntehome.com";
|
||||
|
||||
function settingsHref(path: string) {
|
||||
return `${CAL_SETTINGS_URL.replace(/\/$/, "")}${path}`;
|
||||
}
|
||||
|
||||
function providerLabel(connection: SchedulerConnection): string {
|
||||
return connection.appId ?? connection.type;
|
||||
}
|
||||
|
||||
function ProviderRow({ connection }: { connection: SchedulerConnection }) {
|
||||
return (
|
||||
<div className="provider-row">
|
||||
<span className="provider-name">{providerLabel(connection)}</span>
|
||||
{connection.connected ? (
|
||||
<span className="provider-status connected">Connected</span>
|
||||
) : (
|
||||
<a className="secondary-button" href={settingsHref("/apps/installed")}>
|
||||
Connect
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConnectionsView({ connections }: ConnectionsViewProps) {
|
||||
const calendars = connections.filter((connection) => connection.category === "calendar");
|
||||
const conferencing = connections.filter((connection) => CONFERENCING_CATEGORIES.has(connection.category));
|
||||
|
||||
return (
|
||||
<div className="connections-layout">
|
||||
<header className="panel-header">
|
||||
<h1>Connections</h1>
|
||||
<span className="muted">Reuses every provider supported by Cal.</span>
|
||||
</header>
|
||||
|
||||
<section className="settings-card">
|
||||
<h2>Calendars</h2>
|
||||
{calendars.length === 0 ? (
|
||||
<p className="muted">No calendars connected yet.</p>
|
||||
) : (
|
||||
calendars.map((connection) => (
|
||||
<ProviderRow key={`${connection.type}-${connection.appId}`} connection={connection} />
|
||||
))
|
||||
)}
|
||||
<a className="primary-button browse-button" href={settingsHref("/apps/installed/calendar")}>
|
||||
Browse calendar providers
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<section className="settings-card">
|
||||
<h2>Conferencing</h2>
|
||||
{conferencing.length === 0 ? (
|
||||
<p className="muted">No conferencing providers connected yet.</p>
|
||||
) : (
|
||||
conferencing.map((connection) => (
|
||||
<ProviderRow key={`${connection.type}-${connection.appId}`} connection={connection} />
|
||||
))
|
||||
)}
|
||||
<a className="primary-button browse-button" href={settingsHref("/apps/installed/conferencing")}>
|
||||
Browse conferencing providers
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<p className="privacy-note">Only availability blocks are visible to teammates.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,8 @@ describe("safeSchedulerCallback", () => {
|
||||
});
|
||||
|
||||
it("normalizes redirects for already-authenticated users", () => {
|
||||
expect(safeSchedulerCallback("/connections")).toBe("/connections");
|
||||
expect(safeSchedulerCallback("/connections")).toBe("/calendar");
|
||||
expect(safeSchedulerCallback("/availability")).toBe("/availability");
|
||||
expect(safeSchedulerCallback("https://example.com")).toBe("/calendar");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
const DEFAULT_CALLBACK = "/calendar";
|
||||
const REMOVED_CALLBACKS = new Set(["/connections"]);
|
||||
|
||||
export function safeSchedulerCallback(value: string | string[] | undefined): string {
|
||||
const candidate = Array.isArray(value) ? value[0] : value;
|
||||
if (!candidate || !candidate.startsWith("/") || candidate.startsWith("//")) {
|
||||
if (!candidate || !candidate.startsWith("/") || candidate.startsWith("//") || REMOVED_CALLBACKS.has(candidate)) {
|
||||
return DEFAULT_CALLBACK;
|
||||
}
|
||||
return candidate;
|
||||
|
||||
@@ -71,22 +71,6 @@ export const demoBusyBlocks: BusyBlock[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export type DemoConnection = {
|
||||
type: string;
|
||||
appId: string;
|
||||
category: string;
|
||||
connected: boolean;
|
||||
};
|
||||
|
||||
export const demoConnectionsByUser: Record<number, DemoConnection[]> = {
|
||||
1: [
|
||||
{ type: "google_calendar", appId: "google-calendar", category: "calendar", connected: true },
|
||||
{ type: "google_video", appId: "google-meet", category: "conferencing", connected: true },
|
||||
],
|
||||
2: [{ type: "office365_calendar", appId: "office365-calendar", category: "calendar", connected: true }],
|
||||
3: [],
|
||||
};
|
||||
|
||||
export type DemoMeeting = {
|
||||
id: number;
|
||||
uid: string;
|
||||
|
||||
@@ -4,11 +4,9 @@ import { computeMutualSlots } from "./availability";
|
||||
import { collapseDateOverrides, expandDateOverride } from "./overrides";
|
||||
import {
|
||||
demoBusyBlocks,
|
||||
demoConnectionsByUser,
|
||||
demoMeetings,
|
||||
demoSchedules,
|
||||
demoUsers,
|
||||
type DemoConnection,
|
||||
} from "./demo-data";
|
||||
import { isDemoMode } from "./demo-mode";
|
||||
import { filterBusyBlocksForViewer } from "./privacy";
|
||||
@@ -304,54 +302,6 @@ export async function updateSchedule(userId: number, body: unknown): Promise<Sch
|
||||
return loadSchedule(userId);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connections (no secrets exposed)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type SchedulerConnection = {
|
||||
type: string;
|
||||
appId: string | null;
|
||||
category: string;
|
||||
connected: boolean;
|
||||
};
|
||||
|
||||
export async function getConnections(
|
||||
userId: number
|
||||
): Promise<{ connections: SchedulerConnection[] }> {
|
||||
if (isDemoMode()) {
|
||||
const demo: DemoConnection[] = demoConnectionsByUser[userId] ?? [];
|
||||
return {
|
||||
connections: demo.map((connection) => ({
|
||||
type: connection.type,
|
||||
appId: connection.appId,
|
||||
category: connection.category,
|
||||
connected: connection.connected,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// Only non-secret metadata is selected: `key`/`encryptedKey` are never read.
|
||||
const credentials = await prisma.credential.findMany({
|
||||
where: { userId },
|
||||
select: {
|
||||
type: true,
|
||||
appId: true,
|
||||
invalid: true,
|
||||
app: { select: { categories: true } },
|
||||
},
|
||||
orderBy: { id: "asc" },
|
||||
});
|
||||
|
||||
const connections: SchedulerConnection[] = credentials.map((credential) => ({
|
||||
type: credential.type,
|
||||
appId: credential.appId,
|
||||
category: credential.app?.categories?.[0] ?? "other",
|
||||
connected: credential.invalid !== true,
|
||||
}));
|
||||
|
||||
return { connections };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Availability
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -497,8 +447,6 @@ const createMeetingSchema = z.object({
|
||||
attendeeIds: z.array(z.number().int().positive()).min(1),
|
||||
start: z.string().datetime(),
|
||||
durationMinutes: z.number().int().positive(),
|
||||
location: z.string().optional(),
|
||||
conferencing: z.enum(["none", "provider"]).optional(),
|
||||
});
|
||||
|
||||
export type CreateMeetingResult =
|
||||
|
||||
@@ -52,6 +52,4 @@ export type MeetingDraft = {
|
||||
attendeeIds: number[];
|
||||
start: string;
|
||||
durationMinutes: number;
|
||||
location?: string;
|
||||
conferencing?: "none" | "provider";
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user