Add apiRequest function to auto refresh outdated access token

This commit is contained in:
Melon 2023-12-13 12:54:45 +00:00
parent 3d12a4dda3
commit 47bf91e7ff
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
8 changed files with 155 additions and 79 deletions

35
src/utils/api-request.ts Normal file
View File

@ -0,0 +1,35 @@
import {get} from "svelte/store";
import {getBearer, loginStore} from "../stores/login";
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);
}

View File

@ -2,6 +2,8 @@
import {domainOption} from "../stores/domain-option"; import {domainOption} from "../stores/domain-option";
import {getBearer} from "../stores/login"; import {getBearer} from "../stores/login";
import {type Cert, certsTable} from "../stores/certs"; import {type Cert, certsTable} from "../stores/certs";
import {apiRequest} from "../utils/api-request";
import {onMount} from "svelte";
const apiOrchid = import.meta.env.VITE_API_ORCHID; const apiOrchid = import.meta.env.VITE_API_ORCHID;
@ -27,25 +29,17 @@
return p.endsWith(domain); return p.endsWith(domain);
} }
let promiseForTable: Promise<void> = Object.entries($certsTable).length === 0 ? reloadTable() : Promise.resolve(); let promiseForTable: Promise<void> = Object.entries($certsTable).length === 0 ? reloadTable() : reloadTable();
function reloadTable(): Promise<void> { async function reloadTable(): Promise<void> {
return new Promise<void>((res, rej) => { let f = await apiRequest(apiOrchid + "/owned");
fetch(apiOrchid + "/owned", {headers: {Authorization: getBearer()}}) if (f.status !== 200) throw new Error("Unexpected status code: " + f.status);
.then(x => { let fJson = await f.json();
if (x.status !== 200) throw new Error("Unexpected status code: " + x.status); let rows = fJson as Map<number, Cert>;
return x.json();
})
.then(x => {
let rows = x as Map<number, Cert>;
Object.values(rows).forEach(x => { Object.values(rows).forEach(x => {
$certsTable[Object(x.id).toString()] = x; $certsTable[Object(x.id).toString()] = x;
}); });
console.log($certsTable); console.log($certsTable);
res();
})
.catch(x => rej(x));
});
} }
</script> </script>

View File

@ -1,5 +1,29 @@
<script lang="ts">
import {loginStore, parseJwt} from "../stores/login";
let tokenExp = 0;
let diffExp = 0;
loginStore.subscribe(x => {
if (!x) return;
let jwt = parseJwt(x?.tokens.access);
tokenExp = jwt.exp;
});
function timeDiff(exp: number) {
if (exp === 0) return 0;
return exp * 1000 - new Date().getTime();
}
setInterval(() => {
diffExp = timeDiff(tokenExp);
}, 500);
</script>
<div style="padding:8px;background-color:#bb7900;">Warning: This is currently still under development</div> <div style="padding:8px;background-color:#bb7900;">Warning: This is currently still under development</div>
<div> <div>
<a class="btn-green" href="https://uptime-kuma.1f349.com" target="_blank">Status Dashboard</a> <a class="btn-green" href="https://uptime-kuma.1f349.com" target="_blank">Status Dashboard</a>
</div> </div>
<div>{diffExp === 0 ? "No token" : diffExp}</div>

View File

@ -2,6 +2,7 @@
import {domainOption} from "../stores/domain-option"; import {domainOption} from "../stores/domain-option";
import {getBearer} from "../stores/login"; import {getBearer} from "../stores/login";
import {type Site, sitesTable} from "../stores/sites"; import {type Site, sitesTable} from "../stores/sites";
import {apiRequest} from "../utils/api-request";
const apiSiteHosting = import.meta.env.VITE_API_SITE_HOSTING; const apiSiteHosting = import.meta.env.VITE_API_SITE_HOSTING;
@ -22,51 +23,33 @@
let promiseForTable: Promise<void> = Object.entries($sitesTable).length === 0 ? reloadTable() : Promise.resolve(); let promiseForTable: Promise<void> = Object.entries($sitesTable).length === 0 ? reloadTable() : Promise.resolve();
function reloadTable(): Promise<void> { async function reloadTable(): Promise<void> {
return new Promise<void>((res, rej) => { let f = await apiRequest(apiSiteHosting);
fetch(apiSiteHosting, {headers: {Authorization: getBearer()}}) if (f.status !== 200) throw new Error("Unexpected status code: " + f.status);
.then(x => { let fJson = await f.json();
if (x.status !== 200) throw new Error("Unexpected status code: " + x.status); let rows = fJson as Site[];
return x.json();
})
.then(x => {
let rows = x as Site[];
rows.forEach(x => { rows.forEach(x => {
$sitesTable[x.domain] = x; $sitesTable[x.domain] = x;
}); });
res();
})
.catch(x => rej(x));
});
} }
function deleteBranch(site: Site, branch: string) { async function deleteBranch(site: Site, branch: string) {
fetch(apiSiteHosting, { let f = await apiRequest(apiSiteHosting, {
method: "POST", method: "POST",
headers: {Authorization: getBearer()},
body: JSON.stringify({submit: "delete-branch", site: site.domain, branch}), body: JSON.stringify({submit: "delete-branch", site: site.domain, branch}),
}) });
.then(x => { if (f.status !== 200) throw new Error("Unexpected status code: " + f.status);
if (x.status !== 200) throw new Error("Unexpected status code: " + x.status);
promiseForTable = reloadTable(); promiseForTable = reloadTable();
})
.catch(x => alert("Error deleting branch: " + x));
} }
function resetSiteSecret(site: Site) { async function resetSiteSecret(site: Site) {
fetch(apiSiteHosting, { let f = await apiRequest(apiSiteHosting, {
method: "POST", method: "POST",
headers: {Authorization: getBearer()},
body: JSON.stringify({submit: "secret", site: site.domain}), body: JSON.stringify({submit: "secret", site: site.domain}),
}) });
.then(x => { if (f.status !== 200) throw new Error("Unexpected status code: " + f.status);
if (x.status !== 200) throw new Error("Unexpected status code: " + x.status); let fJson = await f.json();
return x.json(); alert("New secret: " + fJson.secret);
})
.then(x => {
alert("New secret: " + x.secret);
})
.catch(x => alert("Error resetting secret: " + x));
} }
</script> </script>

View File

@ -5,6 +5,8 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import {apiRequest} from "../utils/api-request";
import {writable, type Writable} from "svelte/store"; import {writable, type Writable} from "svelte/store";
import {getBearer} from "../stores/login"; import {getBearer} from "../stores/login";
@ -39,15 +41,12 @@
let promiseForTable: Promise<void> = Object.keys($tableData).length === 0 ? reloadTable() : Promise.resolve(); let promiseForTable: Promise<void> = Object.keys($tableData).length === 0 ? reloadTable() : Promise.resolve();
function reloadTable(): Promise<void> { async function reloadTable(): Promise<void> {
return new Promise<void>((res, rej) => { let f = await apiRequest(apiUrl);
fetch(apiUrl, {headers: {Authorization: getBearer()}}) if (f.status !== 200) throw new Error("Unexpected status code: " + f.status);
.then(x => { let fJson = await f.json();
if (x.status !== 200) throw new Error("Unexpected status code: " + x.status);
return x.json(); let rows = fJson as T[];
})
.then(x => {
let rows = x as T[];
let srcs = new Set(Object.keys($tableData)); let srcs = new Set(Object.keys($tableData));
rows.forEach(x => { rows.forEach(x => {
$tableData[x.src] = { $tableData[x.src] = {
@ -60,10 +59,6 @@
srcs.forEach(x => { srcs.forEach(x => {
$tableData[x].server = null; $tableData[x].server = null;
}); });
res();
})
.catch(x => rej(x));
});
} }
interface Savable<T> { interface Savable<T> {

View File

@ -12,7 +12,7 @@ require (
require github.com/becheran/wildmatch-go v1.0.0 // indirect require github.com/becheran/wildmatch-go v1.0.0 // indirect
require ( require (
github.com/1f349/mjwt v0.2.0 github.com/1f349/mjwt v0.2.1
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@ -1,5 +1,7 @@
github.com/1f349/mjwt v0.2.0 h1:1c3+J05RRBsClGxA91SzT3I2DkwasGA4OgLcIeXWmq4= github.com/1f349/mjwt v0.2.0 h1:1c3+J05RRBsClGxA91SzT3I2DkwasGA4OgLcIeXWmq4=
github.com/1f349/mjwt v0.2.0/go.mod h1:KEs6jd9JjWrQW+8feP2pGAU7pdA3aYTqjkT/YQr73PU= github.com/1f349/mjwt v0.2.0/go.mod h1:KEs6jd9JjWrQW+8feP2pGAU7pdA3aYTqjkT/YQr73PU=
github.com/1f349/mjwt v0.2.1 h1:REdiM/MaNjYQwHvI39LaMPhlvMg4Vy9SgomWMsKTNz8=
github.com/1f349/mjwt v0.2.1/go.mod h1:KEs6jd9JjWrQW+8feP2pGAU7pdA3aYTqjkT/YQr73PU=
github.com/MrMelon54/mjwt v0.1.1 h1:m+aTpxbhQCrOPKHN170DQMFR5r938LkviU38unob5Jw= github.com/MrMelon54/mjwt v0.1.1 h1:m+aTpxbhQCrOPKHN170DQMFR5r938LkviU38unob5Jw=
github.com/MrMelon54/mjwt v0.1.1/go.mod h1:oYrDBWK09Hju98xb+bRQ0wy+RuAzacxYvKYOZchR2Tk= github.com/MrMelon54/mjwt v0.1.1/go.mod h1:oYrDBWK09Hju98xb+bRQ0wy+RuAzacxYvKYOZchR2Tk=
github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=

View File

@ -38,7 +38,7 @@ func ssoServer(signer mjwt.Signer) {
ps.Set("violet:redirect") ps.Set("violet:redirect")
ps.Set("domain:owns=example.com") ps.Set("domain:owns=example.com")
ps.Set("domain:owns=example.org") ps.Set("domain:owns=example.org")
accessToken, err := signer.GenerateJwt("81b99bd7-bf74-4cc2-9133-80ed2393dfe6", uuid.NewString(), jwt.ClaimStrings{"d0555671-df9d-42d0-a4d6-94b694251f0b"}, 15*time.Minute, auth.AccessTokenClaims{ accessToken, err := signer.GenerateJwt("81b99bd7-bf74-4cc2-9133-80ed2393dfe6", uuid.NewString(), jwt.ClaimStrings{"d0555671-df9d-42d0-a4d6-94b694251f0b"}, 10*time.Second, auth.AccessTokenClaims{
Perms: ps, Perms: ps,
}) })
if err != nil { if err != nil {
@ -84,6 +84,49 @@ func ssoServer(signer mjwt.Signer) {
</html> </html>
`, accessToken, "") `, accessToken, "")
}) })
var corsAccessControl = cors.New(cors.Options{
AllowOriginFunc: func(origin string) bool {
println(origin)
return origin == "http://localhost:5173"
},
AllowedMethods: []string{http.MethodPost, http.MethodOptions},
AllowedHeaders: []string{"Content-Type"},
AllowCredentials: true,
})
r.HandleFunc("/refresh", func(w http.ResponseWriter, r *http.Request) {
corsAccessControl.ServeHTTP(w, r, func(w http.ResponseWriter, r *http.Request) {
ps := claims.NewPermStorage()
ps.Set("violet:route")
ps.Set("violet:redirect")
ps.Set("domain:owns=example.com")
ps.Set("domain:owns=example.org")
accessToken, err := signer.GenerateJwt("81b99bd7-bf74-4cc2-9133-80ed2393dfe6", uuid.NewString(), jwt.ClaimStrings{"d0555671-df9d-42d0-a4d6-94b694251f0b"}, 10*time.Second, auth.AccessTokenClaims{
Perms: ps,
})
if err != nil {
http.Error(w, "Failed to generate access token", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]any{
"target": "http://localhost:5173",
"tokens": map[string]any{
"access": accessToken,
"refresh": "",
},
"userinfo": map[string]any{
"aud": "d0555671-df9d-42d0-a4d6-94b694251f0b",
"email": "admin@localhost",
"email_verified": true,
"name": "Admin",
"preferred_username": "admin",
"sub": "81b99bd7-bf74-4cc2-9133-80ed2393dfe6",
"picture": "http://localhost:5173/1f349.svg",
"updated_at": 0,
},
})
})
})
log.Println("[SSO Server]", http.ListenAndServe(":9090", r)) log.Println("[SSO Server]", http.ListenAndServe(":9090", r))
} }