mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +02:00
⭐ feat(infra): Update infra and add support for teams to SST (#186)
## Description - [x] Adds support for AWS SSO, which makes us (the team) able to use SST and update the components independently - [x] Splits the webpage into the landing page (Qwik), and Astro (the console) in charge of playing. This allows us to pass in Environment Variables to the console - ~Migrates the docs from Nuxt to Nextjs, and connects them to SST. This allows us to use Fumadocs _citation needed_ that's much more beautiful, and supports OpenApi~ - Cloudflare pages with github integration is not working on our new CF account. So we will have to push the pages deployment manually with Github actions - [x] Moves the current set up from my personal CF and AWS accounts to dedicated Nestri accounts - ## Related Issues <!-- List any related issues (e.g., "Closes #123", "Fixes #456") --> ## Type of Change - [ ] Bug fix (non-breaking change) - [x] New feature (non-breaking change) - [ ] Breaking change (fix or feature that changes existing functionality) - [x] Documentation update - [ ] Other (please describe): ## Checklist - [x] I have updated relevant documentation - [x] My code follows the project's coding style - [x] My changes generate no new warnings/errors ## Notes for Reviewers <!-- Point out areas you'd like reviewers to focus on, questions you have, or decisions that need discussion --> Please approve my PR 🥹 ## Screenshots/Demo <!-- If applicable, add screenshots or a GIF demo of your changes (especially for UI changes) --> ## Additional Context <!-- Add any other context about the pull request here -->
This commit is contained in:
34
packages/www/src/providers/account.tsx
Normal file
34
packages/www/src/providers/account.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createStore } from "solid-js/store";
|
||||
import { makePersisted } from "@solid-primitives/storage";
|
||||
import { ParentProps, createContext, useContext } from "solid-js";
|
||||
|
||||
type Context = ReturnType<typeof init>;
|
||||
const context = createContext<Context>();
|
||||
|
||||
function init() {
|
||||
const [store, setStore] = makePersisted(
|
||||
createStore({
|
||||
account: "",
|
||||
team: "",
|
||||
dummy: "",
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
value: store,
|
||||
set: setStore,
|
||||
};
|
||||
}
|
||||
|
||||
export function StorageProvider(props: ParentProps) {
|
||||
const ctx = init();
|
||||
return <context.Provider value={ctx}>{props.children}</context.Provider>;
|
||||
}
|
||||
|
||||
export function useStorage() {
|
||||
const ctx = useContext(context);
|
||||
if (!ctx) {
|
||||
throw new Error("No storage context");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
222
packages/www/src/providers/auth.tsx
Normal file
222
packages/www/src/providers/auth.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import { type Team } from "@nestri/core/team/index";
|
||||
import { makePersisted } from "@solid-primitives/storage";
|
||||
import { useLocation, useNavigate } from "@solidjs/router";
|
||||
import { createClient } from "@openauthjs/openauth/client";
|
||||
import { createInitializedContext } from "../common/context";
|
||||
import { createEffect, createMemo, onMount } from "solid-js";
|
||||
import { createStore, produce, reconcile } from "solid-js/store";
|
||||
|
||||
interface AccountInfo {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
access: string;
|
||||
refresh: string;
|
||||
avatarUrl: string;
|
||||
teams: Team.Info[];
|
||||
discriminator: number;
|
||||
polarCustomerID: string | null;
|
||||
}
|
||||
|
||||
interface Storage {
|
||||
accounts: Record<string, AccountInfo>;
|
||||
current?: string;
|
||||
}
|
||||
|
||||
export const client = createClient({
|
||||
issuer: import.meta.env.VITE_AUTH_URL,
|
||||
clientID: "web",
|
||||
});
|
||||
|
||||
export const { use: useAuth, provider: AuthProvider } =
|
||||
createInitializedContext("AuthContext", () => {
|
||||
const [store, setStore] = makePersisted(
|
||||
createStore<Storage>({
|
||||
accounts: {},
|
||||
}),
|
||||
{
|
||||
name: "radiant.auth",
|
||||
},
|
||||
);
|
||||
const location = useLocation();
|
||||
const params = createMemo(
|
||||
() => new URLSearchParams(location.hash.substring(1)),
|
||||
);
|
||||
const accessToken = createMemo(() => params().get("access_token"));
|
||||
const refreshToken = createMemo(() => params().get("refresh_token"));
|
||||
|
||||
|
||||
createEffect(async () => {
|
||||
// if (!result.current && Object.keys(store.accounts).length) {
|
||||
// result.switch(Object.keys(store.accounts)[0])
|
||||
// navigate("/")
|
||||
// }
|
||||
})
|
||||
|
||||
createEffect(async () => {
|
||||
if (accessToken()) return;
|
||||
if (Object.keys(store.accounts).length) return;
|
||||
const redirect = await client.authorize(window.location.origin, "token");
|
||||
window.location.href = redirect.url
|
||||
});
|
||||
|
||||
createEffect(async () => {
|
||||
const current = store.current;
|
||||
const accounts = store.accounts;
|
||||
if (!current) return;
|
||||
const match = accounts[current];
|
||||
if (match) return;
|
||||
const keys = Object.keys(accounts);
|
||||
if (keys.length) {
|
||||
setStore("current", keys[0]);
|
||||
navigate("/");
|
||||
return
|
||||
}
|
||||
const redirect = await client.authorize(window.location.origin, "token");
|
||||
window.location.href = redirect.url
|
||||
});
|
||||
|
||||
async function refresh() {
|
||||
for (const account of [...Object.values(store.accounts)]) {
|
||||
if (!account.refresh) continue;
|
||||
const result = await client.refresh(account.refresh, {
|
||||
access: account.access,
|
||||
})
|
||||
if (result.err) {
|
||||
if ("id" in account)
|
||||
setStore(produce((state) => {
|
||||
delete state.accounts[account.id];
|
||||
}))
|
||||
continue
|
||||
};
|
||||
const tokens = result.tokens || {
|
||||
access: account.access,
|
||||
refresh: account.refresh,
|
||||
}
|
||||
fetch(import.meta.env.VITE_API_URL + "/account", {
|
||||
headers: {
|
||||
authorization: `Bearer ${tokens.access}`,
|
||||
},
|
||||
}).then(async (response) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
const info = await result.data;
|
||||
|
||||
setStore(
|
||||
"accounts",
|
||||
info.id,
|
||||
reconcile({
|
||||
...info,
|
||||
...tokens,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok)
|
||||
setStore(
|
||||
produce((state) => {
|
||||
delete state.accounts[account.id];
|
||||
}),
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (refreshToken() && accessToken()) {
|
||||
const result = await fetch(import.meta.env.VITE_API_URL + "/account", {
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken()}`,
|
||||
},
|
||||
}).catch(() => { })
|
||||
if (result?.ok) {
|
||||
const response = await result.json();
|
||||
const info = await response.data;
|
||||
setStore(
|
||||
"accounts",
|
||||
info.id,
|
||||
reconcile({
|
||||
...info,
|
||||
access: accessToken(),
|
||||
refresh: refreshToken(),
|
||||
}),
|
||||
);
|
||||
setStore("current", info.id);
|
||||
}
|
||||
window.location.hash = "";
|
||||
}
|
||||
|
||||
await refresh();
|
||||
})
|
||||
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// const bar = useCommandBar()
|
||||
|
||||
// bar.register("auth", async () => {
|
||||
// return [
|
||||
// {
|
||||
// category: "Account",
|
||||
// title: "Logout",
|
||||
// icon: IconLogout,
|
||||
// run: async (bar) => {
|
||||
// result.logout();
|
||||
// setStore("current", undefined);
|
||||
// navigate("/");
|
||||
// bar.hide()
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// category: "Add Account",
|
||||
// title: "Add Account",
|
||||
// icon: IconUserAdd,
|
||||
// run: async () => {
|
||||
// const redir = await client.authorize(window.location.origin, "token");
|
||||
// window.location.href = redir.url
|
||||
// bar.hide()
|
||||
// },
|
||||
// },
|
||||
// ...result.all()
|
||||
// .filter((item) => item.id !== result.current.id)
|
||||
// .map((item) => ({
|
||||
// category: "Account",
|
||||
// title: "Switch to " + item.email,
|
||||
// icon: IconUser,
|
||||
// run: async () => {
|
||||
// result.switch(item.id);
|
||||
// navigate("/");
|
||||
// bar.hide()
|
||||
// },
|
||||
// })),
|
||||
// ]
|
||||
// })
|
||||
|
||||
const result = {
|
||||
get current() {
|
||||
return store.accounts[store.current!]!;
|
||||
},
|
||||
switch(accountID: string) {
|
||||
setStore("current", accountID);
|
||||
},
|
||||
all() {
|
||||
return Object.values(store.accounts);
|
||||
},
|
||||
refresh,
|
||||
logout() {
|
||||
setStore(
|
||||
produce((state) => {
|
||||
if (!state.current) return;
|
||||
delete state.accounts[state.current];
|
||||
state.current = Object.keys(state.accounts)[0];
|
||||
}),
|
||||
);
|
||||
},
|
||||
get ready() {
|
||||
return Boolean(!accessToken() && store.current);
|
||||
},
|
||||
};
|
||||
return result;
|
||||
});
|
||||
Reference in New Issue
Block a user