mirror of
https://github.com/1f349/admin.1f349.com.git
synced 2024-11-12 22:51:35 +00:00
New OAuth based login system
This commit is contained in:
parent
11848b2d97
commit
f3d4b0e0a2
@ -1,4 +1,5 @@
|
||||
VITE_SSO_ORIGIN=http://localhost:9090
|
||||
VITE_OAUTH2_CLIENT_ID=abc123
|
||||
|
||||
VITE_API_VIOLET=http://localhost:9095/v1/violet
|
||||
VITE_API_ORCHID=http://localhost:9095/v1/orchid
|
||||
|
@ -1,4 +1,5 @@
|
||||
VITE_SSO_ORIGIN=https://sso.1f349.com
|
||||
VITE_OAUTH2_CLIENT_ID=9b11a141-bcb8-4140-9c88-531a5d7bf15d
|
||||
|
||||
VITE_API_VIOLET=https://api.1f349.com/v1/violet
|
||||
VITE_API_ORCHID=https://api.1f349.com/v1/orchid
|
||||
|
@ -6,9 +6,8 @@
|
||||
import CertificatesView from "./views/CertificatesView.svelte";
|
||||
import SitesView from "./views/SitesView.svelte";
|
||||
import {loginStore, parseJwt, type LoginStore} from "./stores/login";
|
||||
import {openLoginPopup} from "./utils/login-popup";
|
||||
import {domainOption} from "./stores/domain-option";
|
||||
import {apiVerify} from "./utils/api-request";
|
||||
import {LOGIN} from "./utils/login";
|
||||
|
||||
let sidebarOptions: Array<{name: string; view: typeof SvelteComponent<{}>}> = [
|
||||
{name: "General", view: GeneralView},
|
||||
@ -34,7 +33,8 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
apiVerify().catch(() => {});
|
||||
LOGIN.init();
|
||||
LOGIN.userinfo(false);
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -48,7 +48,7 @@
|
||||
</div>
|
||||
{#if $loginStore == null}
|
||||
<div class="login-view">
|
||||
<button on:click={() => openLoginPopup()}>Login</button>
|
||||
<button on:click={() => LOGIN.userinfo(true)}>Login</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="user-view">
|
||||
@ -58,6 +58,7 @@
|
||||
on:click={() => {
|
||||
$loginStore = null;
|
||||
localStorage.removeItem("login-session");
|
||||
localStorage.removeItem("pop2_access_token");
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
|
@ -1,44 +0,0 @@
|
||||
import {get} from "svelte/store";
|
||||
import {getBearer, loginStore} from "../stores/login";
|
||||
|
||||
const TOKEN_VERIFY_API = import.meta.env.VITE_SSO_ORIGIN + "/verify";
|
||||
const TOKEN_REFRESH_API = import.meta.env.VITE_SSO_ORIGIN + "/refresh";
|
||||
|
||||
export async function apiRequest(url: string, init?: RequestInit): Promise<Response> {
|
||||
// setup authorization header
|
||||
if (init == undefined) init = {};
|
||||
init.headers = {...init.headers, Authorization: getBearer()};
|
||||
|
||||
let f = await fetch(url, init);
|
||||
if (f.status !== 403) return f;
|
||||
|
||||
let refreshResp = await fetch(TOKEN_REFRESH_API, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
cache: "no-cache",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({token: get(loginStore)?.tokens.refresh}),
|
||||
});
|
||||
if (refreshResp.status !== 200) {
|
||||
loginStore.set(null);
|
||||
alert("Failed to refresh login session: please login again to continue");
|
||||
throw new Error("403 Unauthorized");
|
||||
}
|
||||
let refreshJson = await refreshResp.json();
|
||||
loginStore.set(refreshJson);
|
||||
|
||||
// update current authorization header
|
||||
init.headers = {...init.headers, Authorization: getBearer()};
|
||||
return await fetch(url, init);
|
||||
}
|
||||
|
||||
export async function apiVerify() {
|
||||
return await apiRequest(TOKEN_VERIFY_API, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
cache: "no-cache",
|
||||
});
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import {loginStore} from "../stores/login";
|
||||
|
||||
let currentLoginPopup: {close: () => void} | null = null;
|
||||
|
||||
const ssoOrigin = import.meta.env.VITE_SSO_ORIGIN;
|
||||
|
||||
window.addEventListener("message", function (event) {
|
||||
if (event.origin !== ssoOrigin) return;
|
||||
if (isObject(event.data)) {
|
||||
loginStore.set(event.data);
|
||||
if (currentLoginPopup) currentLoginPopup.close();
|
||||
return;
|
||||
}
|
||||
alert("Failed to log user in: the login data was probably corrupted");
|
||||
});
|
||||
|
||||
function isObject(obj: Object) {
|
||||
return obj != null && obj.constructor.name === "Object";
|
||||
}
|
||||
|
||||
function popupCenterScreen(url: string, title: string, w: number, h: number, focus: boolean) {
|
||||
const top = (screen.availHeight - h) / 4,
|
||||
left = (screen.availWidth - w) / 2;
|
||||
const popup = openWindow(url, title, `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`);
|
||||
if (focus === true && window.focus !== undefined && popup !== null) popup.focus();
|
||||
return popup;
|
||||
}
|
||||
|
||||
function openWindow(url: string | URL, winnm: string, options: string): Window | null {
|
||||
var wTop = firstAvailableValue([window.screen.availTop, window.screenY, window.screenTop, 0]);
|
||||
var wLeft = firstAvailableValue([window.screen.availLeft, window.screenX, window.screenLeft, 0]);
|
||||
let w: Window | null;
|
||||
var top = 0,
|
||||
left = 0;
|
||||
var result;
|
||||
if ((result = /top=(\d+)/g.exec(options))) top = parseInt(result[1]);
|
||||
if ((result = /left=(\d+)/g.exec(options))) left = parseInt(result[1]);
|
||||
if (options) {
|
||||
options = options.replace("top=" + top, "top=" + (top + wTop));
|
||||
options = options.replace("left=" + left, "left=" + (left + wLeft));
|
||||
w = window.open(url, winnm, options);
|
||||
} else w = window.open(url, winnm);
|
||||
return w;
|
||||
}
|
||||
|
||||
function firstAvailableValue(arr: any) {
|
||||
for (var i = 0; i < arr.length; i++) if (typeof arr[i] != "undefined") return arr[i];
|
||||
}
|
||||
|
||||
export function openLoginPopup() {
|
||||
if (currentLoginPopup) currentLoginPopup.close();
|
||||
currentLoginPopup = popupCenterScreen(ssoOrigin + "/popup?origin=" + encodeURIComponent(location.origin), "Login with 1f349 SSO", 500, 500, false);
|
||||
}
|
33
src/utils/login.ts
Normal file
33
src/utils/login.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import {get} from "svelte/store";
|
||||
import {getBearer, loginStore} from "../stores/login";
|
||||
import {POP2} from "./pop2";
|
||||
|
||||
const TOKEN_AUTHORIZE_API = import.meta.env.VITE_SSO_ORIGIN + "/authorize";
|
||||
const TOKEN_USERINFO_API = import.meta.env.VITE_SSO_ORIGIN + "/userinfo";
|
||||
const OAUTH2_CLIENT_ID = import.meta.env.VITE_OAUTH2_CLIENT_ID;
|
||||
|
||||
export const LOGIN = {
|
||||
init: () => {
|
||||
POP2.init(TOKEN_AUTHORIZE_API, OAUTH2_CLIENT_ID, "openid profile name", 500, 600);
|
||||
},
|
||||
clientRequest: (resource: string, options: RequestInit, refresh: boolean) => {
|
||||
return POP2.clientRequest(resource, options, refresh);
|
||||
},
|
||||
userinfo: (popup: boolean) => {
|
||||
console.info("userinfo", popup);
|
||||
POP2.getToken((token: string) => {
|
||||
POP2.clientRequest(TOKEN_USERINFO_API, {}, popup)
|
||||
.then(x => x.json())
|
||||
.then(x => {
|
||||
console.log(token, x);
|
||||
loginStore.set({
|
||||
userinfo: x,
|
||||
tokens: {access: token, refresh: ""},
|
||||
});
|
||||
})
|
||||
.catch(x => {
|
||||
console.error(x);
|
||||
});
|
||||
}, popup);
|
||||
},
|
||||
};
|
187
src/utils/pop2.js
Normal file
187
src/utils/pop2.js
Normal file
@ -0,0 +1,187 @@
|
||||
/* Simple OAuth 2.0 Client flow library
|
||||
|
||||
Author: MrMelon54, timdream
|
||||
|
||||
Usage:
|
||||
POP2.init(client_id, scope)
|
||||
Initialize the library.
|
||||
redirect_uri is the current page (window.location.href).
|
||||
This function should be put before Analytics so that the second click won't result a page view register.
|
||||
POP2.getToken(callback)
|
||||
Send access token to the callback function as the first argument.
|
||||
If not logged in this triggers login popup and execute login after logged in.
|
||||
Be sure to call this function in user-triggered event (such as click) to prevent popup blocker.
|
||||
If not sure do use isLoggedIn() below to check first.
|
||||
POP2.isLoggedIn()
|
||||
boolean
|
||||
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
export const POP2 = (function (w) {
|
||||
const windowName = "pop2_oauth2_login_popup";
|
||||
|
||||
if (window.name === windowName) {
|
||||
if (window.opener && window.opener.POP2) {
|
||||
if (window.location.hash.indexOf("access_token") !== -1) {
|
||||
window.opener.POP2.receiveToken(
|
||||
window.location.hash.replace(/^.*access_token=([^&]+).*$/, "$1"),
|
||||
parseInt(window.location.hash.replace(/^.*expires_in=([^&]+).*$/, "$1")),
|
||||
);
|
||||
}
|
||||
if (window.location.search.indexOf("error=")) {
|
||||
window.opener.POP2.receiveToken("ERROR");
|
||||
}
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
|
||||
function popupCenterScreen(url, title, w, h) {
|
||||
const top = (screen.availHeight - h) / 4,
|
||||
left = (screen.availWidth - w) / 2;
|
||||
return openWindow(url, title, `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`);
|
||||
}
|
||||
|
||||
function openWindow(url, winnm, options) {
|
||||
const wTop = firstAvailableValue([window.screen.availTop, window.screenY, window.screenTop, 0]);
|
||||
const wLeft = firstAvailableValue([window.screen.availLeft, window.screenX, window.screenLeft, 0]);
|
||||
let top = "0",
|
||||
left = "0",
|
||||
result;
|
||||
if ((result = /top=(\d+)/g.exec(options))) top = parseInt(result[1]);
|
||||
if ((result = /left=(\d+)/g.exec(options))) left = parseInt(result[1]);
|
||||
let w;
|
||||
if (options) {
|
||||
options = options.replace("top=" + top, "top=" + (parseInt(top) + wTop));
|
||||
options = options.replace("left=" + left, "left=" + (parseInt(left) + wLeft));
|
||||
w = window.open(url, winnm, options);
|
||||
} else w = window.open(url, winnm);
|
||||
return w;
|
||||
}
|
||||
|
||||
function firstAvailableValue(arr) {
|
||||
for (let i = 0; i < arr.length; i++) if (typeof arr[i] != "undefined") return arr[i];
|
||||
}
|
||||
|
||||
let client_endpoint,
|
||||
client_id,
|
||||
scope = "",
|
||||
redirect_uri = window.location.href.substr(0, window.location.href.length - window.location.hash.length).replace(/#$/, ""),
|
||||
access_token = localStorage.getItem("pop2_access_token"),
|
||||
callbackWaitForToken,
|
||||
w_width = 400,
|
||||
w_height = 360;
|
||||
|
||||
const POP2 = {
|
||||
// init
|
||||
init: function (f_client_endpoint, f_client_id, f_scope, width, height) {
|
||||
if (!f_client_endpoint) return false;
|
||||
if (!f_client_id) return false;
|
||||
client_endpoint = f_client_endpoint;
|
||||
client_id = f_client_id;
|
||||
if (f_scope) scope = f_scope;
|
||||
if (width) w_width = width;
|
||||
if (height) w_height = height;
|
||||
},
|
||||
// receive token from popup
|
||||
receiveToken: function (token, expires_in) {
|
||||
if (token !== "ERROR") {
|
||||
access_token = token;
|
||||
localStorage.setItem("pop2_access_token", access_token);
|
||||
if (callbackWaitForToken) callbackWaitForToken(access_token);
|
||||
setTimeout(function () {
|
||||
access_token = undefined;
|
||||
localStorage.removeItem("pop2_access_token");
|
||||
}, expires_in * 1000);
|
||||
} else if (token === false) {
|
||||
callbackWaitForToken = undefined;
|
||||
}
|
||||
},
|
||||
// pass the access token to callback
|
||||
// if not logged in this triggers login popup;
|
||||
// use isLoggedIn to check login first to prevent popup blocker
|
||||
getToken: function (callback, popup = true) {
|
||||
if (!client_id || !redirect_uri || !scope) {
|
||||
alert("You need init() first. Check the program flow.");
|
||||
return false;
|
||||
}
|
||||
if (!popup) throw Error("missing access token");
|
||||
if (!access_token) {
|
||||
callbackWaitForToken = callback;
|
||||
popupCenterScreen(
|
||||
client_endpoint +
|
||||
"?response_type=token" +
|
||||
"&redirect_uri=" +
|
||||
encodeURIComponent(redirect_uri) +
|
||||
"&scope=" +
|
||||
encodeURIComponent(scope) +
|
||||
"&client_id=" +
|
||||
encodeURIComponent(client_id),
|
||||
windowName,
|
||||
w_width,
|
||||
w_height,
|
||||
);
|
||||
return false;
|
||||
} else {
|
||||
callback(access_token);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
clientRequest: function (resource, options, refresh = false) {
|
||||
const sendRequest = function () {
|
||||
options.credentials = "include";
|
||||
if (!options.headers) options.headers = {};
|
||||
options.headers["Authorization"] = "Bearer " + access_token;
|
||||
return new Promise(function (res, rej) {
|
||||
fetch(resource, options)
|
||||
.then(function (x) {
|
||||
if (x.status >= 200 && x.status < 300) res(x);
|
||||
else rej(x);
|
||||
})
|
||||
.catch(function (x) {
|
||||
rej(["failed to send request", x]);
|
||||
});
|
||||
});
|
||||
};
|
||||
const resendRequest = function () {
|
||||
return new Promise(function (res, rej) {
|
||||
access_token = undefined;
|
||||
POP2.getToken(function () {
|
||||
sendRequest()
|
||||
.then(function (x) {
|
||||
res(x);
|
||||
})
|
||||
.catch(function (x) {
|
||||
rej(["failed to resend request", x]);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (!refresh) {
|
||||
if (!access_token) return Promise.reject("missing access token");
|
||||
return sendRequest();
|
||||
} else {
|
||||
return new Promise(function (res, rej) {
|
||||
sendRequest()
|
||||
.then(function (x) {
|
||||
res(x);
|
||||
})
|
||||
.catch(function () {
|
||||
resendRequest()
|
||||
.then(function (x) {
|
||||
res(x);
|
||||
})
|
||||
.catch(function (x) {
|
||||
rej(x);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
window.POP2 = POP2;
|
||||
return POP2;
|
||||
})();
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {domainOption} from "../stores/domain-option";
|
||||
import {type Cert, certsTable} from "../stores/certs";
|
||||
import {apiRequest} from "../utils/api-request";
|
||||
import {LOGIN} from "../utils/login";
|
||||
|
||||
const apiOrchid = import.meta.env.VITE_API_ORCHID;
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
let promiseForTable: Promise<void> = reloadTable();
|
||||
|
||||
async function reloadTable(): Promise<void> {
|
||||
let f = await apiRequest(apiOrchid + "/owned");
|
||||
let f = await LOGIN.clientRequest(apiOrchid + "/owned", {}, false);
|
||||
if (f.status !== 200) throw new Error("Unexpected status code: " + f.status);
|
||||
let fJson = await f.json();
|
||||
let rows = fJson as Map<number, Cert>;
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {domainOption} from "../stores/domain-option";
|
||||
import {type Site, sitesTable} from "../stores/sites";
|
||||
import {apiRequest} from "../utils/api-request";
|
||||
import {LOGIN} from "../utils/login";
|
||||
|
||||
const apiSiteHosting = import.meta.env.VITE_API_SITE_HOSTING;
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
let promiseForTable: Promise<void> = reloadTable();
|
||||
|
||||
async function reloadTable(): Promise<void> {
|
||||
let f = await apiRequest(apiSiteHosting);
|
||||
let f = await LOGIN.clientRequest(apiSiteHosting, {}, false);
|
||||
if (f.status !== 200) throw new Error("Unexpected status code: " + f.status);
|
||||
let fJson = await f.json();
|
||||
let rows = fJson as Site[];
|
||||
@ -33,19 +33,27 @@
|
||||
}
|
||||
|
||||
async function deleteBranch(site: Site, branch: string) {
|
||||
let f = await apiRequest(apiSiteHosting, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({submit: "delete-branch", site: site.domain, branch}),
|
||||
});
|
||||
let f = await LOGIN.clientRequest(
|
||||
apiSiteHosting,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({submit: "delete-branch", site: site.domain, branch}),
|
||||
},
|
||||
false,
|
||||
);
|
||||
if (f.status !== 200) throw new Error("Unexpected status code: " + f.status);
|
||||
promiseForTable = reloadTable();
|
||||
}
|
||||
|
||||
async function resetSiteSecret(site: Site) {
|
||||
let f = await apiRequest(apiSiteHosting, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({submit: "secret", site: site.domain}),
|
||||
});
|
||||
let f = await LOGIN.clientRequest(
|
||||
apiSiteHosting,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({submit: "secret", site: site.domain}),
|
||||
},
|
||||
false,
|
||||
);
|
||||
if (f.status !== 200) throw new Error("Unexpected status code: " + f.status);
|
||||
let fJson = await f.json();
|
||||
alert("New secret: " + fJson.secret);
|
||||
|
@ -5,7 +5,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import {apiRequest} from "../utils/api-request";
|
||||
import {LOGIN} from "../utils/login";
|
||||
import {writable, type Writable} from "svelte/store";
|
||||
import type {CSPair} from "../types/cspair";
|
||||
import {domainOption} from "../stores/domain-option";
|
||||
@ -42,7 +42,7 @@
|
||||
let promiseForTable: Promise<void> = reloadTable();
|
||||
|
||||
async function reloadTable(): Promise<void> {
|
||||
let f = await apiRequest(apiUrl);
|
||||
let f = await LOGIN.clientRequest(apiUrl, {}, false);
|
||||
if (f.status !== 200) throw new Error("Unexpected status code: " + f.status);
|
||||
let fJson = await f.json();
|
||||
|
||||
@ -77,10 +77,14 @@
|
||||
})
|
||||
.sort((a, _) => (a.type === "del" ? -1 : a.type === "ins" ? 1 : 0))
|
||||
.map(x => {
|
||||
x.v.p = apiRequest(apiUrl, {
|
||||
method: x.type == "del" ? "DELETE" : "POST",
|
||||
body: JSON.stringify(x.type == "del" ? {src: (x.v.server as T).src} : x.v.client),
|
||||
}).then(x => {
|
||||
x.v.p = LOGIN.clientRequest(
|
||||
apiUrl,
|
||||
{
|
||||
method: x.type == "del" ? "DELETE" : "POST",
|
||||
body: JSON.stringify(x.type == "del" ? {src: (x.v.server as T).src} : x.v.client),
|
||||
},
|
||||
false,
|
||||
).then(x => {
|
||||
if (x.status !== 200) throw new Error("Unexpected status code: " + x.status);
|
||||
});
|
||||
return x.v.p;
|
||||
|
Loading…
Reference in New Issue
Block a user