Work with Dex in an iframe

This commit is contained in:
Julian 2023-04-30 01:36:00 +00:00
parent fbe2222d1b
commit 41033d2b6f
Signed by: NotNite
GPG Key ID: BD91A5402CCEB08A
19 changed files with 378 additions and 1 deletions

5
dex/robots.txt Normal file
View File

@ -0,0 +1,5 @@
User-agent: *
Disallow: /
User-agent: LUN-4
Allow: *

17
dex/static/main.css Normal file
View File

@ -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;
}

View File

@ -0,0 +1,74 @@
{{ template "header.html" . }}
<script>
let source = null;
function sendToGluestick(msg) {
source.postMessage(msg, {
targetOrigin: '{{ print (extra "gluestick_url") }}'
});
}
document.addEventListener("DOMContentLoaded", () => {
window.addEventListener(
"message",
(e) => {
if (e.origin != '{{ print (extra "gluestick_url") }}') return;
switch (e.data.type) {
case "appResult":
submitApproval(e.data.success);
break;
}
},
false
);
const iframe = document.querySelector("iframe");
iframe.addEventListener("load", () => {
source = iframe.contentWindow;
sendToGluestick({ type: "hello" });
sendToGluestick({
type: "appInfo",
client: "{{ .Client }}",
scopes:
{{ if .Scopes }}
[
{{ range $scope := .Scopes }}
"{{ $scope }}",
{{ end }}
]
{{ else }}
null
{{ end }}
});
});
});
async function submitApproval(doesApprove) {
const params = new URLSearchParams();
params.append("req", "{{ .AuthReqID }}");
params.append("approval", doesApprove ? "approve" : "rejected");
// cursed shit to work about cors
const form = document.createElement("form");
form.method = "POST";
const hiddenReq = document.createElement("input");
hiddenReq.type = "hidden";
hiddenReq.name = "req";
hiddenReq.value = "{{ .AuthReqID }}";
form.appendChild(hiddenReq);
const hiddenApproval = document.createElement("input");
hiddenApproval.type = "hidden";
hiddenApproval.name = "approval";
hiddenApproval.value = doesApprove ? "approve" : "rejected";
form.appendChild(hiddenApproval);
document.body.appendChild(form);
form.submit();
}
</script>
<iframe src='{{ print (extra "gluestick_url") "/dex/approval" }}'></iframe>
{{ template "footer.html" . }}

View File

@ -0,0 +1 @@
{{ template "header.html" . }} {{ template "footer.html" . }}

View File

@ -0,0 +1 @@
{{ template "header.html" . }} {{ template "footer.html" . }}

1
dex/templates/error.html Normal file
View File

@ -0,0 +1 @@
{{ template "header.html" . }} {{ template "footer.html" . }}

View File

@ -0,0 +1,2 @@
</body>
</html>

11
dex/templates/header.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>gluestick</title>
<link rel="icon" href='{{ print (extra "gluestick_url") "/icon.svg" }}' />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<link href='{{ url .ReqPath "static/main.css" }}' rel="stylesheet" />
</head>
<body>

1
dex/templates/login.html Normal file
View File

@ -0,0 +1 @@
{{ template "header.html" . }} {{ template "footer.html" . }}

1
dex/templates/oob.html Normal file
View File

@ -0,0 +1 @@
{{ template "header.html" . }} {{ template "footer.html" . }}

View File

@ -0,0 +1,61 @@
{{ template "header.html" . }}
<script>
let source = null;
function sendToGluestick(msg) {
source.postMessage(msg, {
targetOrigin: '{{ print (extra "gluestick_url") }}'
});
}
document.addEventListener("DOMContentLoaded", () => {
window.addEventListener(
"message",
(e) => {
if (e.origin != '{{ print (extra "gluestick_url") }}') return;
switch (e.data.type) {
case "passwordSubmit":
const { username, password } = e.data;
submitLogin(username, password);
break;
}
},
false
);
const iframe = document.querySelector("iframe");
iframe.addEventListener("load", () => {
source = iframe.contentWindow;
sendToGluestick({ type: "hello" });
});
});
async function submitLogin(username, password) {
const params = new URLSearchParams();
params.append("login", username);
params.append("password", password);
const req = await fetch("{{ .PostURL }}", {
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: params,
method: "POST",
redirect: "follow"
});
if (req.ok && req.redirected) {
window.location.href = req.url;
return;
}
sendToGluestick({
type: "passwordSubmitResult",
success: req.ok
});
}
</script>
<iframe src='{{ print (extra "gluestick_url") "/dex/password" }}'></iframe>
{{ template "footer.html" . }}

1
environment.d.ts vendored
View File

@ -27,6 +27,7 @@ declare global {
GITHUB_ORG: string; GITHUB_ORG: string;
BASE_DOMAIN: string; BASE_DOMAIN: string;
DEX_DOMAIN: string;
} }
} }
} }

View File

@ -3,7 +3,18 @@ const nextConfig = {
experimental: { experimental: {
appDir: true 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; module.exports = nextConfig;

View File

@ -0,0 +1,10 @@
.buttons {
display: flex;
width: 100%;
justify-content: center;
gap: 1rem;
}
.approvalText {
padding: 1rem 0;
}

View File

@ -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<AppInfo | null>(null);
const sendToDex = useDex(domain, (msg) => {
switch (msg.type) {
case "appInfo":
setAppInfo(msg);
break;
}
});
if (appInfo === null) return <></>;
// Stolen from LoginForm
return (
<div>
<h1>Sign into {appInfo.client}</h1>
<div className={styles.approvalText}>
{appInfo.scopes != null ? (
<>
<p>{appInfo.client} would like to:</p>
<ul>
{appInfo.scopes.map((scope) => (
<li key={scope}>{scope}</li>
))}
</ul>
</>
) : (
<p>{appInfo.client} doesn't have any special permissions.</p>
)}
</div>
<div className={styles.buttons}>
<input
type="submit"
value="Allow"
onClick={() => {
sendToDex({ type: "appResult", success: true });
}}
/>
<input
type="submit"
value="Deny"
onClick={() => {
sendToDex({ type: "appResult", success: false });
}}
/>
</div>
</div>
);
}

View File

@ -0,0 +1,10 @@
import styles from "@/app/page.module.css";
import DexApprovalForm from "./DexApprovalForm";
export default async function Page() {
return (
<main className={styles.main}>
<DexApprovalForm domain={process.env.DEX_DOMAIN} />
</main>
);
}

41
src/app/dex/dex.tsx Normal file
View File

@ -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<MessageEventSource | null>(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;
}

View File

@ -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<string | null>(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 (
<PrettyForm globalError={globalError}>
<Formik
initialValues={{ username: "", password: "" }}
onSubmit={handleFormSubmit}
validationSchema={loginSchema}
>
{() => (
<Form>
<Input
type="text"
placeholder="julian"
name="username"
label="Username"
/>
<Input
type="password"
placeholder="deeznuts47"
name="password"
label="Password"
/>
<input type="submit" value="Login" disabled={submitting} />
</Form>
)}
</Formik>
</PrettyForm>
);
}

View File

@ -0,0 +1,10 @@
import styles from "@/app/page.module.css";
import DexPasswordForm from "./DexPasswordForm";
export default async function Page() {
return (
<main className={styles.main}>
<DexPasswordForm domain={process.env.DEX_DOMAIN} />
</main>
);
}