From 097fbfd4c9aa199de6385991b2bf87f9e9de5073 Mon Sep 17 00:00:00 2001 From: Arjun Komath <arjun@hey.com> Date: Thu, 9 Jan 2025 18:15:43 +1100 Subject: [PATCH] Logto API integration --- .../calendar/[ownerId]/[projectId]/route.ts | 7 +- app/(dashboard)/[tenant]/layout.tsx | 8 +-- app/(dashboard)/[tenant]/settings/actions.ts | 5 ++ components/console/navbar.tsx | 3 +- components/core/auth.tsx | 14 ++-- lib/ops/auth.ts | 71 +++++++++++-------- lib/utils/useOwner.ts | 8 ++- package.json | 1 + pnpm-lock.yaml | 14 +++- 9 files changed, 80 insertions(+), 51 deletions(-) diff --git a/app/(api)/api/calendar/[ownerId]/[projectId]/route.ts b/app/(api)/api/calendar/[ownerId]/[projectId]/route.ts index 17a4eb1..b609293 100644 --- a/app/(api)/api/calendar/[ownerId]/[projectId]/route.ts +++ b/app/(api)/api/calendar/[ownerId]/[projectId]/route.ts @@ -1,5 +1,6 @@ import { calendarEvent, project, task, taskList } from "@/drizzle/schema"; import { getDatabaseForOwner } from "@/lib/utils/useDatabase"; +import { getVtimezoneComponent } from "@touch4it/ical-timezones"; import { and, desc, eq, lte } from "drizzle-orm"; import ical, { ICalCalendarMethod } from "ical-generator"; @@ -46,7 +47,7 @@ export async function GET( const calendar = ical({ name: projectDetails.name, method: ICalCalendarMethod.PUBLISH, - timezone: { name: "UTC" }, + timezone: { name: "Australia/Sydney", generator: getVtimezoneComponent }, }); for (const event of events) { @@ -60,7 +61,7 @@ export async function GET( created: event.createdAt, lastModified: event.updatedAt, repeating: event.repeatRule, - timezone: "UTC", + timezone: "Australia/Sydney", }); } @@ -82,7 +83,7 @@ export async function GET( allDay: true, created: task.createdAt, lastModified: task.updatedAt, - timezone: "UTC", + timezone: "Australia/Sydney", }); } } diff --git a/app/(dashboard)/[tenant]/layout.tsx b/app/(dashboard)/[tenant]/layout.tsx index 767dd21..3dc41a4 100644 --- a/app/(dashboard)/[tenant]/layout.tsx +++ b/app/(dashboard)/[tenant]/layout.tsx @@ -1,7 +1,7 @@ import NavBar from "@/components/console/navbar"; import { ReportTimezone } from "@/components/core/report-timezone"; import { isDatabaseReady } from "@/lib/utils/useDatabase"; -import { getOrgs, getOwner } from "@/lib/utils/useOwner"; +import { getOrganizations, getOwner } from "@/lib/utils/useOwner"; import { redirect } from "next/navigation"; export const fetchCache = "force-no-store"; // disable cache for console pages @@ -19,8 +19,8 @@ export default async function ConsoleLayout(props: { const ready = await isDatabaseReady(); const { orgId, orgSlug } = await getOwner(); - const orgs = await getOrgs(); - const activatedOrg = orgs.find((org) => org.id === orgId) ?? null; + const organizations = await getOrganizations(); + const activatedOrg = organizations.find((org) => org.id === orgId) ?? null; if (!ready || params.tenant !== orgSlug) { redirect("/start"); @@ -28,7 +28,7 @@ export default async function ConsoleLayout(props: { return ( <div className="relative flex min-h-full flex-col"> - <NavBar orgs={orgs} activeOrg={activatedOrg} /> + <NavBar orgs={organizations} activeOrg={activatedOrg} /> <div className="mx-auto w-full flex-grow lg:flex"> <div className="min-w-0 flex-1 xl:flex"> diff --git a/app/(dashboard)/[tenant]/settings/actions.ts b/app/(dashboard)/[tenant]/settings/actions.ts index bc1e69b..1448a3f 100644 --- a/app/(dashboard)/[tenant]/settings/actions.ts +++ b/app/(dashboard)/[tenant]/settings/actions.ts @@ -1,5 +1,7 @@ "use server"; +import { updateUser } from "@/lib/ops/auth"; +import { getOwner } from "@/lib/utils/useOwner"; import { cookies } from "next/headers"; export async function saveUserTimezone(timezone: string) { @@ -9,4 +11,7 @@ export async function saveUserTimezone(timezone: string) { sameSite: "strict", maxAge: 60 * 60 * 24 * 365, }); + + const { userId } = await getOwner(); + await updateUser(userId, { customData: { timezone } }); } diff --git a/components/console/navbar.tsx b/components/console/navbar.tsx index 7387f52..9dbd579 100644 --- a/components/console/navbar.tsx +++ b/components/console/navbar.tsx @@ -1,7 +1,8 @@ +import type { Organization } from "@/lib/ops/auth"; import Image from "next/image"; import Link from "next/link"; import logo from "../../public/images/logo.png"; -import { OrgSwitcher, type Organization, UserButton } from "../core/auth"; +import { OrgSwitcher, UserButton } from "../core/auth"; import NavBarLinks from "./navbar-links"; export default function NavBar({ diff --git a/components/core/auth.tsx b/components/core/auth.tsx index d70c84d..e4618d8 100644 --- a/components/core/auth.tsx +++ b/components/core/auth.tsx @@ -1,4 +1,5 @@ import { logtoConfig } from "@/app/logto"; +import type { Organization } from "@/lib/ops/auth"; import { signOut } from "@logto/next/server-actions"; import { ChevronsUpDown, Plus, User } from "lucide-react"; import Link from "next/link"; @@ -12,13 +13,6 @@ import { DropdownMenuTrigger, } from "../ui/dropdown-menu"; -// WIP, this should be changed -export type Organization = { - id: string; - name: string; - slug: string; -}; - export const OrgSwitcher = ({ orgs, activeOrg, @@ -67,7 +61,11 @@ export const OrgSwitcher = ({ // } > <input type="hidden" name="id" value={org.id} /> - <input type="hidden" name="slug" value={org.slug} /> + <input + type="hidden" + name="slug" + value={String(org.customData?.slug)} + /> <button type="submit" className="flex w-full" diff --git a/lib/ops/auth.ts b/lib/ops/auth.ts index 56cb59f..17efc2d 100644 --- a/lib/ops/auth.ts +++ b/lib/ops/auth.ts @@ -4,6 +4,31 @@ const applicationId = process.env.LOGTO_M2M_APP_ID!; const applicationSecret = process.env.LOGTO_M2M_APP_SECRET!; const tenantId = "default"; +/** + * Docs + * https://openapi.logto.io + */ + +export interface Organization { + tenantId: string; + id: string; + name: string; + description: string; + customData: Record<string, string | number | boolean>; + isMfaRequired: boolean; + branding: { + logoUrl: string; + darkLogoUrl: string; + favicon: string; + darkFavicon: string; + }; + createdAt: number; + organizationRoles: { + id: string; + name: string; + }[]; +} + export const fetchAccessToken = async () => { const { endpoint } = logtoConfig; return await fetch(`${endpoint}oidc/token`, { @@ -21,10 +46,13 @@ export const fetchAccessToken = async () => { }).toString(), }); }; - -export const createOrganizationForUser = async ( +export const updateUser = async ( userId: string, - name: string, + data: { + name?: string; + primaryEmail?: string; + customData?: Record<string, unknown>; + }, ) => { const { access_token } = await fetchAccessToken().then((res) => res.json()); if (!access_token) { @@ -32,48 +60,33 @@ export const createOrganizationForUser = async ( } const { endpoint } = logtoConfig; - const response = await fetch(`${endpoint}api/organizations`, { - method: "POST", + const response = await fetch(`${endpoint}api/users/${userId}`, { + method: "PATCH", headers: { Authorization: `Bearer ${access_token}`, "Content-Type": "application/json", }, - body: JSON.stringify({ - name, - }), + body: JSON.stringify(data), }); if (!response.ok) { - console.error("Failed to create organization", response.status); - throw new Error("Failed to create organization"); + console.error("Failed to update user", response.status); + throw new Error("Failed to update user"); } - const organization = await response.json(); - console.log("organization", organization); - - // Add user to organization - await fetch(`${endpoint}api/organizations/${organization.id}/users`, { - method: "POST", - headers: { - Authorization: `Bearer ${access_token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - userIds: [userId], - }), - }); - - return organization; + return await response.json(); }; -export const getOrganizationsForUser = async (userId: string) => { +export const getOrganizationsForUser = async ( + userId: string, +): Promise<Organization[]> => { const { access_token } = await fetchAccessToken().then((res) => res.json()); if (!access_token) { throw new Error("Access token not found"); } const { endpoint } = logtoConfig; - const response = await fetch(`${endpoint}api/organizations`, { + const response = await fetch(`${endpoint}api/users/${userId}/organizations`, { method: "GET", headers: { Authorization: `Bearer ${access_token}`, @@ -85,9 +98,7 @@ export const getOrganizationsForUser = async (userId: string) => { console.error("Failed to fetch organizations", response.status); throw new Error("Failed to fetch organizations"); } - const organizations = await response.json(); - console.log("organizations", organizations); return organizations; }; diff --git a/lib/utils/useOwner.ts b/lib/utils/useOwner.ts index f5892fe..7c54322 100644 --- a/lib/utils/useOwner.ts +++ b/lib/utils/useOwner.ts @@ -1,5 +1,4 @@ import { logtoConfig } from "@/app/logto"; -import type { Organization } from "@/components/core/auth"; import { user } from "@/drizzle/schema"; import type { User } from "@/drizzle/types"; import { getLogtoContext } from "@logto/next/server-actions"; @@ -8,6 +7,7 @@ import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; import { eq } from "drizzle-orm"; import { cookies } from "next/headers"; +import { type Organization, getOrganizationsForUser } from "../ops/auth"; import { database } from "./useDatabase"; dayjs.extend(utc); @@ -39,8 +39,10 @@ export async function getUser(): Promise<User> { return userDetails; } -export async function getOrgs(): Promise<Organization[]> { - return []; +export async function getOrganizations(): Promise<Organization[]> { + const { userId } = await getOwner(); + const organizations = await getOrganizationsForUser(userId); + return organizations; } export async function getOwner(): Promise<Result> { diff --git a/package.json b/package.json index 12303b5..343856d 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", + "@touch4it/ical-timezones": "^1.9.0", "autoprefixer": "10.4.14", "better-sqlite3": "^11.7.0", "class-variance-authority": "^0.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec78671..01a7ddc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,9 @@ importers: '@radix-ui/react-switch': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@touch4it/ical-timezones': + specifier: ^1.9.0 + version: 1.9.0 autoprefixer: specifier: 10.4.14 version: 10.4.14(postcss@8.4.23) @@ -101,7 +104,7 @@ importers: version: 15.1.0(eslint@9.13.0(jiti@1.21.6))(typescript@5.7.2) ical-generator: specifier: ^8.0.0 - version: 8.0.0(@types/node@20.1.0)(dayjs@1.11.12)(rrule@2.8.1) + version: 8.0.0(@touch4it/ical-timezones@1.9.0)(@types/node@20.1.0)(dayjs@1.11.12)(rrule@2.8.1) lucide-react: specifier: ^0.244.0 version: 0.244.0(react@19.0.0) @@ -2150,6 +2153,10 @@ packages: '@tanstack/virtual-core@3.8.4': resolution: {integrity: sha512-iO5Ujgw3O1yIxWDe9FgUPNkGjyT657b1WNX52u+Wv1DyBFEpdCdGkuVaky0M3hHFqNWjAmHWTn4wgj9rTr7ZQg==} + '@touch4it/ical-timezones@1.9.0': + resolution: {integrity: sha512-UAiZMrFlgMdOIaJDPsKu5S7OecyMLr3GGALJTYkRgHmsHAA/8Ixm1qD09ELP2X7U1lqgrctEgvKj9GzMbczC+g==} + engines: {node: '>= 14.0.0'} + '@types/better-sqlite3@7.6.12': resolution: {integrity: sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==} @@ -6845,6 +6852,8 @@ snapshots: '@tanstack/virtual-core@3.8.4': {} + '@touch4it/ical-timezones@1.9.0': {} + '@types/better-sqlite3@7.6.12': dependencies: '@types/node': 20.1.0 @@ -8057,10 +8066,11 @@ snapshots: html-url-attributes@3.0.1: {} - ical-generator@8.0.0(@types/node@20.1.0)(dayjs@1.11.12)(rrule@2.8.1): + ical-generator@8.0.0(@touch4it/ical-timezones@1.9.0)(@types/node@20.1.0)(dayjs@1.11.12)(rrule@2.8.1): dependencies: uuid-random: 1.3.2 optionalDependencies: + '@touch4it/ical-timezones': 1.9.0 '@types/node': 20.1.0 dayjs: 1.11.12 rrule: 2.8.1