Work with Dex in an iframe
This commit is contained in:
parent
fbe2222d1b
commit
41033d2b6f
|
@ -0,0 +1,5 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: LUN-4
|
||||||
|
Allow: *
|
|
@ -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;
|
||||||
|
}
|
|
@ -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" . }}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ template "header.html" . }} {{ template "footer.html" . }}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ template "header.html" . }} {{ template "footer.html" . }}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ template "header.html" . }} {{ template "footer.html" . }}
|
|
@ -0,0 +1,2 @@
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
||||||
|
{{ template "header.html" . }} {{ template "footer.html" . }}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ template "header.html" . }} {{ template "footer.html" . }}
|
|
@ -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" . }}
|
|
@ -27,6 +27,7 @@ declare global {
|
||||||
GITHUB_ORG: string;
|
GITHUB_ORG: string;
|
||||||
|
|
||||||
BASE_DOMAIN: string;
|
BASE_DOMAIN: string;
|
||||||
|
DEX_DOMAIN: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approvalText {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue