diff --git a/dex/robots.txt b/dex/robots.txt new file mode 100644 index 0000000..43bc21d --- /dev/null +++ b/dex/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Disallow: / + +User-agent: LUN-4 +Allow: * diff --git a/dex/static/main.css b/dex/static/main.css new file mode 100644 index 0000000..25de9c0 --- /dev/null +++ b/dex/static/main.css @@ -0,0 +1,17 @@ +body, +html { + margin: 0; + padding: 0; + width: 100vw; + height: 100vh; + + overflow: hidden; +} + +iframe { + width: 100vw; + height: 100vh; + border: none; + margin: 0; + padding: 0; +} diff --git a/dex/templates/approval.html b/dex/templates/approval.html new file mode 100644 index 0000000..85f0bc7 --- /dev/null +++ b/dex/templates/approval.html @@ -0,0 +1,74 @@ +{{ template "header.html" . }} + + + + +{{ template "footer.html" . }} diff --git a/dex/templates/device.html b/dex/templates/device.html new file mode 100644 index 0000000..8a51fb3 --- /dev/null +++ b/dex/templates/device.html @@ -0,0 +1 @@ +{{ template "header.html" . }} {{ template "footer.html" . }} diff --git a/dex/templates/device_success.html b/dex/templates/device_success.html new file mode 100644 index 0000000..8a51fb3 --- /dev/null +++ b/dex/templates/device_success.html @@ -0,0 +1 @@ +{{ template "header.html" . }} {{ template "footer.html" . }} diff --git a/dex/templates/error.html b/dex/templates/error.html new file mode 100644 index 0000000..8a51fb3 --- /dev/null +++ b/dex/templates/error.html @@ -0,0 +1 @@ +{{ template "header.html" . }} {{ template "footer.html" . }} diff --git a/dex/templates/footer.html b/dex/templates/footer.html new file mode 100644 index 0000000..b605728 --- /dev/null +++ b/dex/templates/footer.html @@ -0,0 +1,2 @@ + + diff --git a/dex/templates/header.html b/dex/templates/header.html new file mode 100644 index 0000000..2bd5000 --- /dev/null +++ b/dex/templates/header.html @@ -0,0 +1,11 @@ + + + + + gluestick + + + + + + \ No newline at end of file diff --git a/dex/templates/login.html b/dex/templates/login.html new file mode 100644 index 0000000..8a51fb3 --- /dev/null +++ b/dex/templates/login.html @@ -0,0 +1 @@ +{{ template "header.html" . }} {{ template "footer.html" . }} diff --git a/dex/templates/oob.html b/dex/templates/oob.html new file mode 100644 index 0000000..8a51fb3 --- /dev/null +++ b/dex/templates/oob.html @@ -0,0 +1 @@ +{{ template "header.html" . }} {{ template "footer.html" . }} diff --git a/dex/templates/password.html b/dex/templates/password.html new file mode 100644 index 0000000..578e2a8 --- /dev/null +++ b/dex/templates/password.html @@ -0,0 +1,61 @@ +{{ template "header.html" . }} + + + + +{{ template "footer.html" . }} diff --git a/environment.d.ts b/environment.d.ts index 000a0b4..8943719 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -27,6 +27,7 @@ declare global { GITHUB_ORG: string; BASE_DOMAIN: string; + DEX_DOMAIN: string; } } } diff --git a/next.config.js b/next.config.js index 182ff7a..08d72fc 100644 --- a/next.config.js +++ b/next.config.js @@ -3,7 +3,18 @@ const nextConfig = { experimental: { appDir: true }, - output: "standalone" + output: "standalone", + + // Allow Dex to use gluestick in an iframe + headers: async () => { + return [{ + source: "/dex/(.*)", + headers: [{ + key: "Content-Security-Policy", + value: `frame-ancestors 'self' ${process.env.DEX_DOMAIN}` + }] + }] + } }; module.exports = nextConfig; diff --git a/src/app/dex/approval/DexApprovalForm.module.css b/src/app/dex/approval/DexApprovalForm.module.css new file mode 100644 index 0000000..8640a65 --- /dev/null +++ b/src/app/dex/approval/DexApprovalForm.module.css @@ -0,0 +1,10 @@ +.buttons { + display: flex; + width: 100%; + justify-content: center; + gap: 1rem; +} + +.approvalText { + padding: 1rem 0; +} diff --git a/src/app/dex/approval/DexApprovalForm.tsx b/src/app/dex/approval/DexApprovalForm.tsx new file mode 100644 index 0000000..4e4a5e6 --- /dev/null +++ b/src/app/dex/approval/DexApprovalForm.tsx @@ -0,0 +1,59 @@ +"use client"; + +import React from "react"; +import { AppInfo, useDex } from "../dex"; +import styles from "./DexApprovalForm.module.css"; + +export default function DexApprovalForm({ domain }: { domain: string }) { + const [appInfo, setAppInfo] = React.useState(null); + + const sendToDex = useDex(domain, (msg) => { + switch (msg.type) { + case "appInfo": + setAppInfo(msg); + break; + } + }); + + if (appInfo === null) return <>; + + // Stolen from LoginForm + return ( +
+

Sign into {appInfo.client}

+ +
+ {appInfo.scopes != null ? ( + <> +

{appInfo.client} would like to:

+
    + {appInfo.scopes.map((scope) => ( +
  • {scope}
  • + ))} +
+ + ) : ( +

{appInfo.client} doesn't have any special permissions.

+ )} +
+ +
+ { + sendToDex({ type: "appResult", success: true }); + }} + /> + + { + sendToDex({ type: "appResult", success: false }); + }} + /> +
+
+ ); +} diff --git a/src/app/dex/approval/page.tsx b/src/app/dex/approval/page.tsx new file mode 100644 index 0000000..8a1feb5 --- /dev/null +++ b/src/app/dex/approval/page.tsx @@ -0,0 +1,10 @@ +import styles from "@/app/page.module.css"; +import DexApprovalForm from "./DexApprovalForm"; + +export default async function Page() { + return ( +
+ +
+ ); +} diff --git a/src/app/dex/dex.tsx b/src/app/dex/dex.tsx new file mode 100644 index 0000000..4c3e799 --- /dev/null +++ b/src/app/dex/dex.tsx @@ -0,0 +1,41 @@ +import React from "react"; + +export type AppInfo = { + type: "appInfo"; + client: string; + scopes: string[] | null; +}; + +export type Message = + | { type: "hello" } + | { type: "passwordSubmit"; username: string; password: string } + | { type: "passwordSubmitResult"; success: boolean } + | AppInfo + | { type: "appResult"; success: boolean }; + +export function useDex(domain: string, handler: (msg: Message) => void) { + const [source, setSource] = React.useState(null); + + const sendToDex = (msg: Message, maybeSource?: MessageEventSource) => { + let realSource = maybeSource ?? source; + realSource!.postMessage(msg, { + targetOrigin: domain + }); + }; + + React.useEffect(() => { + window.addEventListener("message", (e) => { + if (e.origin !== domain) return; + const message: Message = e.data; + setSource(e.source); + + if (message.type === "hello") { + sendToDex({ type: "hello" }, e.source!); + } + + handler(message); + }); + }, []); + + return sendToDex; +} diff --git a/src/app/dex/password/DexPasswordForm.tsx b/src/app/dex/password/DexPasswordForm.tsx new file mode 100644 index 0000000..4248125 --- /dev/null +++ b/src/app/dex/password/DexPasswordForm.tsx @@ -0,0 +1,60 @@ +"use client"; + +import React from "react"; +import { useDex } from "../dex"; +import { LoginFormValues, loginSchema } from "@/schemas"; +import { Form, Formik } from "formik"; +import PrettyForm from "@/components/PrettyForm"; +import Input from "@/components/Input"; + +export default function DexPasswordForm({ domain }: { domain: string }) { + const [globalError, setGlobalError] = React.useState(null); + const [submitting, setSubmitting] = React.useState(false); + + const sendToDex = useDex(domain, (msg) => { + switch (msg.type) { + case "passwordSubmitResult": + setSubmitting(false); + if (!msg.success) setGlobalError("Invalid credentials."); + break; + } + }); + + async function handleFormSubmit({ username, password }: LoginFormValues) { + setSubmitting(true); + sendToDex({ + type: "passwordSubmit", + username, + password + }); + } + + // Stolen from LoginForm + return ( + + + {() => ( +
+ + + +
+ )} +
+
+ ); +} diff --git a/src/app/dex/password/page.tsx b/src/app/dex/password/page.tsx new file mode 100644 index 0000000..2945687 --- /dev/null +++ b/src/app/dex/password/page.tsx @@ -0,0 +1,10 @@ +import styles from "@/app/page.module.css"; +import DexPasswordForm from "./DexPasswordForm"; + +export default async function Page() { + return ( +
+ +
+ ); +}