mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
fix(den): refresh desktop sign-in state and support local handoff (#1389)
* fix(den): make desktop signin state update immediately * fix(den): support local auth verification and handoff --------- Co-authored-by: src-opn <src-opn@users.noreply.github.com>
This commit is contained in:
@@ -16,6 +16,11 @@ import {
|
||||
resolveDenBaseUrls,
|
||||
writeDenSettings,
|
||||
} from "../lib/den";
|
||||
import {
|
||||
denSessionUpdatedEvent,
|
||||
dispatchDenSessionUpdated,
|
||||
type DenSessionUpdatedDetail,
|
||||
} from "../lib/den-session-events";
|
||||
import {
|
||||
clearDenTemplateCache,
|
||||
loadDenTemplateCache,
|
||||
@@ -78,9 +83,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
|
||||
const platform = usePlatform();
|
||||
const tr = (key: string) => t(key, currentLocale());
|
||||
const initial = readDenSettings();
|
||||
const initialBaseUrl = props.developerMode
|
||||
? initial.baseUrl || DEFAULT_DEN_BASE_URL
|
||||
: DEFAULT_DEN_BASE_URL;
|
||||
const initialBaseUrl = initial.baseUrl || DEFAULT_DEN_BASE_URL;
|
||||
|
||||
const [baseUrl, setBaseUrl] = createSignal(initialBaseUrl);
|
||||
const [baseUrlDraft, setBaseUrlDraft] = createSignal(initialBaseUrl);
|
||||
@@ -155,7 +158,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
|
||||
|
||||
createEffect(() => {
|
||||
writeDenSettings({
|
||||
baseUrl: props.developerMode ? baseUrl() : DEFAULT_DEN_BASE_URL,
|
||||
baseUrl: baseUrl(),
|
||||
authToken: authToken() || null,
|
||||
activeOrgId: activeOrgId() || null,
|
||||
activeOrgSlug: activeOrg()?.slug ?? null,
|
||||
@@ -163,14 +166,6 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
|
||||
});
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!props.developerMode) {
|
||||
setBaseUrl(DEFAULT_DEN_BASE_URL);
|
||||
setBaseUrlDraft(DEFAULT_DEN_BASE_URL);
|
||||
setBaseUrlError(null);
|
||||
}
|
||||
});
|
||||
|
||||
const openControlPlane = () => {
|
||||
platform.openLink(resolveDenBaseUrls(baseUrl()).baseUrl);
|
||||
};
|
||||
@@ -239,7 +234,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
|
||||
}
|
||||
|
||||
writeDenSettings({
|
||||
baseUrl: props.developerMode ? nextBaseUrl : DEFAULT_DEN_BASE_URL,
|
||||
baseUrl: nextBaseUrl,
|
||||
authToken: result.token,
|
||||
activeOrgId: null,
|
||||
activeOrgSlug: null,
|
||||
@@ -248,26 +243,21 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
|
||||
|
||||
setManualAuthInput("");
|
||||
setManualAuthOpen(false);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("openwork-den-session-updated", {
|
||||
detail: {
|
||||
status: "success",
|
||||
email: result.user?.email ?? null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
dispatchDenSessionUpdated({
|
||||
status: "success",
|
||||
baseUrl: nextBaseUrl,
|
||||
token: result.token,
|
||||
user: result.user,
|
||||
email: result.user?.email ?? null,
|
||||
});
|
||||
} catch (error) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("openwork-den-session-updated", {
|
||||
detail: {
|
||||
status: "error",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: tr("den.error_signin_failed"),
|
||||
},
|
||||
}),
|
||||
);
|
||||
dispatchDenSessionUpdated({
|
||||
status: "error",
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: tr("den.error_signin_failed"),
|
||||
});
|
||||
} finally {
|
||||
setAuthBusy(false);
|
||||
}
|
||||
@@ -337,7 +327,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
|
||||
const nextOrg = response.orgs.find((org) => org.id === next) ?? null;
|
||||
setActiveOrgId(next);
|
||||
writeDenSettings({
|
||||
baseUrl: props.developerMode ? baseUrl() : DEFAULT_DEN_BASE_URL,
|
||||
baseUrl: baseUrl(),
|
||||
authToken: authToken() || null,
|
||||
activeOrgId: next || null,
|
||||
activeOrgSlug: nextOrg?.slug ?? null,
|
||||
@@ -472,18 +462,27 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
|
||||
|
||||
createEffect(() => {
|
||||
const handler = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{
|
||||
status?: string;
|
||||
email?: string | null;
|
||||
message?: string | null;
|
||||
}>;
|
||||
const customEvent = event as CustomEvent<DenSessionUpdatedDetail>;
|
||||
const nextSettings = readDenSettings();
|
||||
setBaseUrl(nextSettings.baseUrl || DEFAULT_DEN_BASE_URL);
|
||||
setBaseUrlDraft(nextSettings.baseUrl || DEFAULT_DEN_BASE_URL);
|
||||
setAuthToken(nextSettings.authToken?.trim() || "");
|
||||
const nextBaseUrl =
|
||||
customEvent.detail?.baseUrl?.trim() ||
|
||||
nextSettings.baseUrl ||
|
||||
DEFAULT_DEN_BASE_URL;
|
||||
const nextToken =
|
||||
customEvent.detail?.token?.trim() ||
|
||||
nextSettings.authToken?.trim() ||
|
||||
"";
|
||||
setBaseUrl(nextBaseUrl);
|
||||
setBaseUrlDraft(nextBaseUrl);
|
||||
setAuthToken(nextToken);
|
||||
setActiveOrgId(nextSettings.activeOrgId?.trim() || "");
|
||||
if (customEvent.detail?.status === "success") {
|
||||
clearSessionState();
|
||||
if (customEvent.detail.user) {
|
||||
setUser(customEvent.detail.user);
|
||||
}
|
||||
setAuthError(null);
|
||||
setSessionBusy(false);
|
||||
setStatusMessage(
|
||||
customEvent.detail.email?.trim()
|
||||
? t("den.status_cloud_signed_in_as", currentLocale(), { email: customEvent.detail.email.trim() })
|
||||
@@ -498,12 +497,12 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
|
||||
};
|
||||
|
||||
window.addEventListener(
|
||||
"openwork-den-session-updated",
|
||||
denSessionUpdatedEvent,
|
||||
handler as EventListener,
|
||||
);
|
||||
return () =>
|
||||
window.removeEventListener(
|
||||
"openwork-den-session-updated",
|
||||
denSessionUpdatedEvent,
|
||||
handler as EventListener,
|
||||
);
|
||||
});
|
||||
@@ -828,7 +827,7 @@ export default function DenSettingsPanel(props: DenSettingsPanelProps) {
|
||||
const nextOrg = orgs().find((org) => org.id === nextId) ?? null;
|
||||
setActiveOrgId(nextId);
|
||||
writeDenSettings({
|
||||
baseUrl: props.developerMode ? baseUrl() : DEFAULT_DEN_BASE_URL,
|
||||
baseUrl: baseUrl(),
|
||||
authToken: authToken() || null,
|
||||
activeOrgId: nextId || null,
|
||||
activeOrgSlug: nextOrg?.slug ?? null,
|
||||
|
||||
24
apps/app/src/app/lib/den-session-events.ts
Normal file
24
apps/app/src/app/lib/den-session-events.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { DenUser } from "./den";
|
||||
|
||||
export const denSessionUpdatedEvent = "openwork-den-session-updated";
|
||||
|
||||
export type DenSessionUpdatedDetail = {
|
||||
status?: "success" | "error";
|
||||
baseUrl?: string | null;
|
||||
token?: string | null;
|
||||
user?: DenUser | null;
|
||||
email?: string | null;
|
||||
message?: string | null;
|
||||
};
|
||||
|
||||
export function dispatchDenSessionUpdated(detail: DenSessionUpdatedDetail) {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent<DenSessionUpdatedDetail>(denSessionUpdatedEvent, {
|
||||
detail,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -198,6 +198,35 @@ export function normalizeDenBaseUrl(input: string | null | undefined): string |
|
||||
|
||||
function isWebAppHost(hostname: string): boolean {
|
||||
const normalized = hostname.trim().toLowerCase();
|
||||
|
||||
if (
|
||||
normalized === "localhost" ||
|
||||
normalized === "0.0.0.0" ||
|
||||
normalized === "::1" ||
|
||||
normalized === "[::1]" ||
|
||||
/^127(?:\.\d{1,3}){3}$/.test(normalized)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const ipv4Match = normalized.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
||||
if (ipv4Match) {
|
||||
const [first, second, third, fourth] = ipv4Match.slice(1).map(Number);
|
||||
const octets = [first, second, third, fourth];
|
||||
if (octets.every((octet) => Number.isInteger(octet) && octet >= 0 && octet <= 255)) {
|
||||
if (
|
||||
first === 10 ||
|
||||
first === 127 ||
|
||||
(first === 172 && second >= 16 && second <= 31) ||
|
||||
(first === 192 && second === 168) ||
|
||||
(first === 169 && second === 254) ||
|
||||
(first === 100 && second >= 64 && second <= 127)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return normalized === "app.openworklabs.com" || normalized === "app.openwork.software" || normalized.startsWith("app.");
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createEffect, createSignal, type Accessor } from "solid-js";
|
||||
import { t, currentLocale } from "../../i18n";
|
||||
|
||||
import { createDenClient, writeDenSettings } from "../lib/den";
|
||||
import { dispatchDenSessionUpdated } from "../lib/den-session-events";
|
||||
import { stripBundleQuery } from "../bundles";
|
||||
import type { createBundlesStore } from "../bundles/store";
|
||||
import type { SettingsTab, View } from "../types";
|
||||
@@ -35,6 +36,15 @@ export function createDeepLinksController(options: {
|
||||
const [pendingDenAuthDeepLink, setPendingDenAuthDeepLink] = createSignal<DenAuthDeepLink | null>(null);
|
||||
const [processingDenAuthDeepLink, setProcessingDenAuthDeepLink] = createSignal(false);
|
||||
const recentClaimedDeepLinks = new Map<string, number>();
|
||||
const recentHandledDenGrants = new Map<string, number>();
|
||||
|
||||
const pruneRecentHandledDenGrants = (now: number) => {
|
||||
for (const [grant, seenAt] of recentHandledDenGrants) {
|
||||
if (now - seenAt > 5 * 60 * 1000) {
|
||||
recentHandledDenGrants.delete(grant);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const queueRemoteConnectDefaults = (pending: RemoteWorkspaceDefaults | null) => {
|
||||
setPendingRemoteConnectDeepLink(pending);
|
||||
@@ -87,6 +97,18 @@ export function createDeepLinksController(options: {
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
pruneRecentHandledDenGrants(now);
|
||||
if (recentHandledDenGrants.has(parsed.grant)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentPending = pendingDenAuthDeepLink();
|
||||
if (currentPending?.grant === parsed.grant) {
|
||||
return true;
|
||||
}
|
||||
|
||||
setPendingDenAuthDeepLink(parsed);
|
||||
return true;
|
||||
};
|
||||
@@ -173,6 +195,7 @@ export function createDeepLinksController(options: {
|
||||
|
||||
setProcessingDenAuthDeepLink(true);
|
||||
setPendingDenAuthDeepLink(null);
|
||||
recentHandledDenGrants.set(pending.grant, Date.now());
|
||||
options.setView("settings");
|
||||
options.setSettingsTab("den");
|
||||
options.goToSettings("den");
|
||||
@@ -192,24 +215,20 @@ export function createDeepLinksController(options: {
|
||||
activeOrgName: null,
|
||||
});
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("openwork-den-session-updated", {
|
||||
detail: {
|
||||
status: "success",
|
||||
email: result.user?.email ?? null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
dispatchDenSessionUpdated({
|
||||
status: "success",
|
||||
baseUrl: pending.denBaseUrl,
|
||||
token: result.token,
|
||||
user: result.user,
|
||||
email: result.user?.email ?? null,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("openwork-den-session-updated", {
|
||||
detail: {
|
||||
status: "error",
|
||||
message: error instanceof Error ? error.message : t("app.error_cloud_signin", currentLocale()),
|
||||
},
|
||||
}),
|
||||
);
|
||||
recentHandledDenGrants.delete(pending.grant);
|
||||
dispatchDenSessionUpdated({
|
||||
status: "error",
|
||||
message: error instanceof Error ? error.message : t("app.error_cloud_signin", currentLocale()),
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setProcessingDenAuthDeepLink(false);
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import { db } from "./db.js"
|
||||
import { env } from "./env.js"
|
||||
import { sendDenOrganizationInvitationEmail, sendDenVerificationEmail } from "./email.js"
|
||||
import { syncDenSignupContact } from "./loops.js"
|
||||
import { db } from "./db.js";
|
||||
import { env } from "./env.js";
|
||||
import {
|
||||
sendDenOrganizationInvitationEmail,
|
||||
sendDenVerificationEmail,
|
||||
} from "./email.js";
|
||||
import { syncDenSignupContact } from "./loops.js";
|
||||
import {
|
||||
DEN_API_KEY_DEFAULT_PREFIX,
|
||||
DEN_API_KEY_RATE_LIMIT_MAX,
|
||||
DEN_API_KEY_RATE_LIMIT_TIME_WINDOW_MS,
|
||||
} from "./api-keys.js"
|
||||
import { denOrganizationAccess, denOrganizationStaticRoles } from "./organization-access.js"
|
||||
import { seedDefaultOrganizationRoles } from "./orgs.js"
|
||||
import { createDenTypeId, normalizeDenTypeId } from "@openwork-ee/utils/typeid"
|
||||
import * as schema from "@openwork-ee/den-db/schema"
|
||||
import { apiKey } from "@better-auth/api-key"
|
||||
import { APIError } from "better-call"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle"
|
||||
import { emailOTP, organization } from "better-auth/plugins"
|
||||
} from "./api-keys.js";
|
||||
import {
|
||||
denOrganizationAccess,
|
||||
denOrganizationStaticRoles,
|
||||
} from "./organization-access.js";
|
||||
import { seedDefaultOrganizationRoles } from "./orgs.js";
|
||||
import { createDenTypeId, normalizeDenTypeId } from "@openwork-ee/utils/typeid";
|
||||
import * as schema from "@openwork-ee/den-db/schema";
|
||||
import { apiKey } from "@better-auth/api-key";
|
||||
import { APIError } from "better-call";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { emailOTP, organization } from "better-auth/plugins";
|
||||
|
||||
const socialProviders = {
|
||||
...(env.github.clientId && env.github.clientSecret
|
||||
@@ -34,29 +40,39 @@ const socialProviders = {
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
};
|
||||
|
||||
function hasRole(roleValue: string, roleName: string) {
|
||||
return roleValue
|
||||
.split(",")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
.includes(roleName)
|
||||
.includes(roleName);
|
||||
}
|
||||
|
||||
function getInvitationOrigin() {
|
||||
return env.betterAuthTrustedOrigins.find((origin) => origin !== "*") ?? env.betterAuthUrl
|
||||
return (
|
||||
env.betterAuthTrustedOrigins.find((origin) => origin !== "*") ??
|
||||
env.betterAuthUrl
|
||||
);
|
||||
}
|
||||
|
||||
function buildInvitationLink(invitationId: string) {
|
||||
return new URL(`/join-org?invite=${encodeURIComponent(invitationId)}`, getInvitationOrigin()).toString()
|
||||
return new URL(
|
||||
`/join-org?invite=${encodeURIComponent(invitationId)}`,
|
||||
getInvitationOrigin(),
|
||||
).toString();
|
||||
}
|
||||
|
||||
export const auth = betterAuth({
|
||||
baseURL: env.betterAuthUrl,
|
||||
secret: env.betterAuthSecret,
|
||||
trustedOrigins: env.betterAuthTrustedOrigins.length > 0 ? env.betterAuthTrustedOrigins : undefined,
|
||||
socialProviders: Object.keys(socialProviders).length > 0 ? socialProviders : undefined,
|
||||
trustedOrigins:
|
||||
env.betterAuthTrustedOrigins.length > 0
|
||||
? env.betterAuthTrustedOrigins
|
||||
: undefined,
|
||||
socialProviders:
|
||||
Object.keys(socialProviders).length > 0 ? socialProviders : undefined,
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "mysql",
|
||||
schema,
|
||||
@@ -70,32 +86,32 @@ export const auth = betterAuth({
|
||||
generateId: (options) => {
|
||||
switch (options.model) {
|
||||
case "user":
|
||||
return createDenTypeId("user")
|
||||
return createDenTypeId("user");
|
||||
case "session":
|
||||
return createDenTypeId("session")
|
||||
return createDenTypeId("session");
|
||||
case "account":
|
||||
return createDenTypeId("account")
|
||||
return createDenTypeId("account");
|
||||
case "verification":
|
||||
return createDenTypeId("verification")
|
||||
return createDenTypeId("verification");
|
||||
case "apikey":
|
||||
case "apiKey":
|
||||
return createDenTypeId("apiKey")
|
||||
return createDenTypeId("apiKey");
|
||||
case "rateLimit":
|
||||
return createDenTypeId("rateLimit")
|
||||
return createDenTypeId("rateLimit");
|
||||
case "organization":
|
||||
return createDenTypeId("organization")
|
||||
return createDenTypeId("organization");
|
||||
case "member":
|
||||
return createDenTypeId("member")
|
||||
return createDenTypeId("member");
|
||||
case "invitation":
|
||||
return createDenTypeId("invitation")
|
||||
return createDenTypeId("invitation");
|
||||
case "team":
|
||||
return createDenTypeId("team")
|
||||
return createDenTypeId("team");
|
||||
case "teamMember":
|
||||
return createDenTypeId("teamMember")
|
||||
return createDenTypeId("teamMember");
|
||||
case "organizationRole":
|
||||
return createDenTypeId("organizationRole")
|
||||
return createDenTypeId("organizationRole");
|
||||
default:
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -135,7 +151,7 @@ export const auth = betterAuth({
|
||||
await syncDenSignupContact({
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
})
|
||||
});
|
||||
},
|
||||
},
|
||||
emailAndPassword: {
|
||||
@@ -150,14 +166,10 @@ export const auth = betterAuth({
|
||||
expiresIn: 600,
|
||||
allowedAttempts: 5,
|
||||
async sendVerificationOTP({ email, otp, type }) {
|
||||
if (type !== "email-verification") {
|
||||
return
|
||||
}
|
||||
|
||||
await sendDenVerificationEmail({
|
||||
email,
|
||||
verificationCode: otp,
|
||||
})
|
||||
});
|
||||
},
|
||||
}),
|
||||
organization({
|
||||
@@ -182,30 +194,33 @@ export const auth = betterAuth({
|
||||
invitedByEmail: data.inviter.user.email,
|
||||
organizationName: data.organization.name,
|
||||
role: data.role,
|
||||
})
|
||||
});
|
||||
},
|
||||
organizationHooks: {
|
||||
afterCreateOrganization: async ({ organization }) => {
|
||||
await seedDefaultOrganizationRoles(normalizeDenTypeId("organization", organization.id))
|
||||
await seedDefaultOrganizationRoles(
|
||||
normalizeDenTypeId("organization", organization.id),
|
||||
);
|
||||
},
|
||||
beforeRemoveMember: async ({ member }) => {
|
||||
if (hasRole(member.role, "owner")) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "The organization owner cannot be removed.",
|
||||
})
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeUpdateMemberRole: async ({ member, newRole }) => {
|
||||
if (hasRole(member.role, "owner")) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "The organization owner role cannot be changed.",
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (hasRole(newRole, "owner")) {
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Owner can only be assigned during organization creation.",
|
||||
})
|
||||
message:
|
||||
"Owner can only be assigned during organization creation.",
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -224,4 +239,4 @@ export const auth = betterAuth({
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
@@ -46,6 +46,35 @@ function readSingleHeader(value: string | null) {
|
||||
|
||||
function isWebAppHost(hostname: string) {
|
||||
const normalized = hostname.trim().toLowerCase()
|
||||
|
||||
if (
|
||||
normalized === "localhost"
|
||||
|| normalized === "0.0.0.0"
|
||||
|| normalized === "::1"
|
||||
|| normalized === "[::1]"
|
||||
|| /^127(?:\.\d{1,3}){3}$/.test(normalized)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
const ipv4Match = normalized.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
|
||||
if (ipv4Match) {
|
||||
const [first, second, third, fourth] = ipv4Match.slice(1).map(Number)
|
||||
const octets = [first, second, third, fourth]
|
||||
if (octets.every((octet) => Number.isInteger(octet) && octet >= 0 && octet <= 255)) {
|
||||
if (
|
||||
first === 10
|
||||
|| first === 127
|
||||
|| (first === 172 && second >= 16 && second <= 31)
|
||||
|| (first === 192 && second === 168)
|
||||
|| (first === 169 && second === 254)
|
||||
|| (first === 100 && second >= 64 && second <= 127)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return normalized === "app.openworklabs.com"
|
||||
|| normalized === "app.openwork.software"
|
||||
|| normalized.startsWith("app.")
|
||||
@@ -178,51 +207,75 @@ export function registerDesktopAuthRoutes<T extends { Variables: AuthContextVari
|
||||
const input = c.req.valid("json")
|
||||
|
||||
const now = new Date()
|
||||
const rows = await db
|
||||
.select({
|
||||
grant: DesktopHandoffGrantTable,
|
||||
session: AuthSessionTable,
|
||||
user: AuthUserTable,
|
||||
})
|
||||
.from(DesktopHandoffGrantTable)
|
||||
.innerJoin(AuthSessionTable, eq(DesktopHandoffGrantTable.session_token, AuthSessionTable.token))
|
||||
.innerJoin(AuthUserTable, eq(DesktopHandoffGrantTable.user_id, AuthUserTable.id))
|
||||
.where(
|
||||
and(
|
||||
eq(DesktopHandoffGrantTable.id, input.grant),
|
||||
isNull(DesktopHandoffGrantTable.consumed_at),
|
||||
gt(DesktopHandoffGrantTable.expires_at, now),
|
||||
gt(AuthSessionTable.expiresAt, now),
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
const exchange = await db.transaction(async (tx) => {
|
||||
const rows = await tx
|
||||
.select({
|
||||
session: AuthSessionTable,
|
||||
user: AuthUserTable,
|
||||
})
|
||||
.from(DesktopHandoffGrantTable)
|
||||
.innerJoin(AuthSessionTable, eq(DesktopHandoffGrantTable.session_token, AuthSessionTable.token))
|
||||
.innerJoin(AuthUserTable, eq(DesktopHandoffGrantTable.user_id, AuthUserTable.id))
|
||||
.where(
|
||||
and(
|
||||
eq(DesktopHandoffGrantTable.id, input.grant),
|
||||
isNull(DesktopHandoffGrantTable.consumed_at),
|
||||
gt(DesktopHandoffGrantTable.expires_at, now),
|
||||
gt(AuthSessionTable.expiresAt, now),
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
const row = rows[0]
|
||||
if (!row) {
|
||||
const row = rows[0]
|
||||
if (!row) {
|
||||
return null
|
||||
}
|
||||
|
||||
const consumedAt = new Date()
|
||||
await tx
|
||||
.update(DesktopHandoffGrantTable)
|
||||
.set({ consumed_at: consumedAt })
|
||||
.where(
|
||||
and(
|
||||
eq(DesktopHandoffGrantTable.id, input.grant),
|
||||
isNull(DesktopHandoffGrantTable.consumed_at),
|
||||
gt(DesktopHandoffGrantTable.expires_at, now),
|
||||
),
|
||||
)
|
||||
|
||||
const claimed = await tx
|
||||
.select({ id: DesktopHandoffGrantTable.id })
|
||||
.from(DesktopHandoffGrantTable)
|
||||
.where(
|
||||
and(
|
||||
eq(DesktopHandoffGrantTable.id, input.grant),
|
||||
eq(DesktopHandoffGrantTable.consumed_at, consumedAt),
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!claimed[0]) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
token: row.session.token,
|
||||
user: {
|
||||
id: row.user.id,
|
||||
email: row.user.email,
|
||||
name: row.user.name,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
if (!exchange) {
|
||||
return c.json({
|
||||
error: "grant_not_found",
|
||||
message: "This desktop sign-in link is missing, expired, or already used.",
|
||||
}, 404)
|
||||
}
|
||||
|
||||
await db
|
||||
.update(DesktopHandoffGrantTable)
|
||||
.set({ consumed_at: now })
|
||||
.where(
|
||||
and(
|
||||
eq(DesktopHandoffGrantTable.id, input.grant),
|
||||
isNull(DesktopHandoffGrantTable.consumed_at),
|
||||
),
|
||||
)
|
||||
|
||||
return c.json({
|
||||
token: row.session.token,
|
||||
user: {
|
||||
id: row.user.id,
|
||||
email: row.user.email,
|
||||
name: row.user.name,
|
||||
},
|
||||
})
|
||||
return c.json(exchange)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user