[CHORE] Implement RedirectURIs
Some checks failed
S3 Repo Backup / s3-repo-backup (push) Failing after 1s
Deploy Edge / deploy-edge (push) Failing after 8s

This commit is contained in:
Dunemask 2024-08-24 16:51:30 -06:00
parent 0fc5f05b6a
commit fdb19be2ef
12 changed files with 91 additions and 33 deletions

View file

@ -1 +1,8 @@
node_modules/
node_modules/
templates/
dist/
.helmignore
Chart.yaml
values.yaml
.forgejo/
.git/

View file

@ -16,10 +16,10 @@ jobs:
- name: Oasis Setup
uses: https://forgejo.dunemask.dev/elysium/elysium-actions@oasis-setup-auto
with:
deploy-env: edge
deploy-env: prod
infisical-token: ${{ secrets.INFISICAL_ELYSIUM_EDGE_READ_TOKEN }}
extra-secret-paths: /dashboard,/alexandria
extra-secret-envs: edge,edge
extra-secret-paths: /dashboard,/devops,/kubernetes
extra-secret-envs: prod,prod,prod
# Deploy to Edge
- name: Deploy to Edge env
run: garden deploy $GARDEN_DEPLOY_ACTION --force --force-build --env usw-edge

View file

@ -26,3 +26,6 @@ src/
build/
public/
lib/
prisma/
package.json
package-lock.json

View file

@ -1,5 +1,6 @@
FROM node:20-bookworm-slim
WORKDIR /dunemask/net/cairo
RUN apt-get update -y && apt-get install -y openssl
COPY package.json .
COPY package-lock.json .
COPY .npmrc .

View file

@ -36,13 +36,13 @@ export default class AuthController extends VixpressController {
if (projectKeyPairs.length !== 1) throw ProjectErrors.BadRequestProjectIncomplete;
const token = await getUserToken(user.id, user.project.keyPairs[0].encryptedPrivateKey);
const policies = user.rolePolicy.policies;
const userData: CDatabaseContract["User"] = { username: user.username, rolePolicyId: user.rolePolicyId };
return { token, user: userData, policies };
const usr: CDatabaseContract["User"] = { id: user.id, username: user.username, rolePolicyId: user.rolePolicyId };
return { token, user: usr, policies };
}
async credentials(crc: ContractRouteContext): Promise<CAuthContract["Credentials"]> {
const { user, policies } = crc.req as UserRequest;
const userData: CDatabaseContract["User"] = { username: user.username, rolePolicyId: user.rolePolicyId };
return { user: userData, policies: ResourcePolicy.asStrings(policies) };
const usr: CDatabaseContract["User"] = { id: user.id, username: user.username, rolePolicyId: user.rolePolicyId };
return { user: usr, policies: ResourcePolicy.asStrings(policies) };
}
}

View file

@ -41,8 +41,8 @@ export default class ProjectController extends VixpressController {
const kp = await this.pg.keypair.byUsage(proj.id, KeyPairType.UserToken);
if (!kp) throw ProjectErrors.BadRequestProjectIncomplete;
if (!user) throw ProjectErrors.UnexpectedRootUserError;
const userData: CDatabaseContract["User"] = { username: user.username, rolePolicyId: user.rolePolicyId };
const usr: CDatabaseContract["User"] = { id: user.id, username: user.username, rolePolicyId: user.rolePolicyId };
const publicKey = await decrypt(kp.encryptedPublicKey, config.SigningOptions.Keys.KeyPair);
return { user: userData, project: proj, publicKey };
return { user: usr, project: proj, publicKey };
}
}

View file

@ -7,6 +7,7 @@ const antiRequired = y.string().test("insecure-exposure", "Insecure Exposure", (
export const DatabaseContractRes = defineContractExport("CDatabaseContractRes", {
User: y.object({
id: y.string().required(),
username: y.string().required(),
email: y.string().nullable(),
hash: antiRequired,

View file

@ -1,6 +1,6 @@
import { lazy, LazyExoticComponent, ReactNode, Suspense } from "react";
import { lazy, LazyExoticComponent, ReactNode, Suspense, useEffect } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { Route, Routes } from "react-router-dom";
import { Route, Routes, useSearchParams } from "react-router-dom";
import { CenteredLoadingSpinner } from "./components/common/Loading";
import { CenteredErrorFallback } from "./components/common/Fallback";
import { Links, rootLink } from "./util/links";
@ -17,9 +17,9 @@ import AuthorizedView from "./views/AuthorizedView";
declare type Portal = { path: Links; view: ReactNode };
function Auth(props: { view: (() => ReactNode) | LazyExoticComponent<() => ReactNode> }) {
const { auth } = useAuth();
const { auth, loading, initialized } = useAuth();
const Component = props.view;
if (!!auth) return <Component />;
if (!loading && !initialized && !!auth) return <Component />;
return <AutoRedirect />;
}

View file

@ -3,9 +3,10 @@ import { apiRequest } from "@dunemask/vix/bridge";
import { Policy, PolicyString } from "@lib/Policies";
import { Resource } from "@lib/vix/AppResources";
import { CDatabaseContract } from "@lib/contracts/database.contracts";
import { useSearchParams } from "react-router-dom";
const project = import.meta.env.VITE_CAIRO_PROJECT as string;
const credentialApiPath = `/${project}/auth/credentials`;
const credentialApiPath = `auth/credentials`;
export enum AuthStorageKeys {
USER = "user",
@ -147,16 +148,19 @@ function authReducer(state: AuthState, action: Action): AuthState {
return state;
}
export async function getPolicies(token: string): Promise<Policy[]> {
const extraHeaders = { Authorization: `Bearer ${token}` };
const credentials = await apiRequest({ subpath: credentialApiPath, jsonify: true, extraHeaders });
if (!credentials) return [];
const { policies } = credentials as CredentialDTO;
return Policy.parseResourcePolicies<Resource>(policies);
}
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(authReducer, initialState);
const [search] = useSearchParams();
async function getPolicies(token: string): Promise<Policy[]> {
const extraHeaders = { Authorization: `Bearer ${token}` };
const projectId = !!search.get("projectId") ? search.get("projectId") : project;
const apiPath = `/${projectId}/${credentialApiPath}`;
const credentials = await apiRequest({ subpath: apiPath, jsonify: true, extraHeaders });
if (!credentials) return [];
const { policies } = credentials as CredentialDTO;
return Policy.parseResourcePolicies<Resource>(policies);
}
async function login(userData: CDatabaseContract["User"], token: string, policies?: Policy[]) {
dispatch({ type: AuthAction.LOADING, loading: true });
@ -179,7 +183,9 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
async function fetchCredentials(token: string): Promise<[user: CDatabaseContract["User"], rp: Policy[]]> {
const extraHeaders = getAuthHeader(token);
const credentials = await apiRequest({ subpath: credentialApiPath, jsonify: true, extraHeaders });
const projectId = !!search.get("projectId") ? search.get("projectId") : project;
const apiPath = `/${projectId}/${credentialApiPath}`;
const credentials = await apiRequest({ subpath: apiPath, jsonify: true, extraHeaders });
if (!credentials) throw Error("Could not authenticate!");
const { user, policies } = credentials as CredentialDTO;
if (!user || !policies) throw Error("Could not authenticate!");

View file

@ -21,6 +21,8 @@ export function useLinkNav() {
}
export function useAutoRedirect() {
const nav = useLinkNav();
return () => nav(Links.AutoRedirect);
const search = window.location.search;
const nav = useNavigate();
const redirectLink = `${rootLink(Links.AutoRedirect)}${search}`;
return () => nav(redirectLink);
}

View file

@ -4,24 +4,28 @@ import { Resource } from "@lib/vix/AppResources";
import { SyntheticEvent, useState } from "react";
import { toast } from "react-toastify";
import { useAuth } from "@src/ctx/AuthContext";
import { useAutoRedirect } from "@src/util/links";
import { Links, rootLink, useAutoRedirect } from "@src/util/links";
import { ClientError } from "@dunemask/vix/bridge";
import { AuthErrors } from "@lib/vix/ClientErrors";
import { PasswordInput } from "@src/components/common/Inputs";
import { ResourcePolicyType } from "@dunemask/vix/util";
import { postProjectAuthLogin } from "@src/util/api/GeneratedRequests";
import { Navigate, useSearchParams } from "react-router-dom";
const project = import.meta.env.VITE_CAIRO_PROJECT as string;
export default function AuthenticateView() {
const { auth, login } = useAuth();
const autoRedirect = useAutoRedirect();
const [search] = useSearchParams();
const [identity, setIdentity] = useState<string>("");
const [password, setPassword] = useState<string>("");
const projectId = search.get("projectId") ?? project;
const identityChange = (e: SyntheticEvent) => setIdentity((e.target as HTMLInputElement).value);
function submitCredentials() {
const loginPromise = postProjectAuthLogin(project, { identity: identity, password }).then(async (creds) => {
const loginPromise = postProjectAuthLogin(projectId, { identity: identity, password }).then(async (creds) => {
if (!creds.token) return toast.error("Server didn't provide token!");
await login(
creds.user,
@ -48,7 +52,7 @@ export default function AuthenticateView() {
function detectEnter(e: KeyboardEvent) {
if (e.key === "Enter") submitCredentials();
}
if (auth) return;
if (auth) return <Navigate to={rootLink(Links.AutoRedirect)} />;
return (
<Box width="100%" height="90vh" display="flex" alignItems="center" justifyContent="center">
<Box p="2rem" width="100%" maxWidth="350px" boxShadow="md" borderRadius="md" bg="background.paper">

View file

@ -4,15 +4,22 @@ import { useAuth } from "@src/ctx/AuthContext";
import useContentGuard, { GuardedContent } from "@src/util/guards";
import { Links, rootLink } from "@src/util/links";
import { useEffect, useState } from "react";
import { Navigate } from "react-router-dom";
import { Navigate, useSearchParams } from "react-router-dom";
const { search } = window.location;
const redirectLink = (l: Links) => `${rootLink(l)}${search}`;
export default function AutoRedirect() {
const manageProjects = useContentGuard(AppGuard.ManageProjects);
const [serachParams] = useSearchParams();
const redirectUri = serachParams.get("redirectUri");
const { auth, initialized, loading } = useAuth();
if (!initialized || loading) return <RedirectLoader />;
if (!auth) return <Navigate to={rootLink(Links.Authenticate)} />;
if (manageProjects) return <Navigate to={rootLink(Links.ProjectView)} />;
return <Navigate to={rootLink(Links.Authorized)} />;
if (!!redirectUri) return <RedirectPortal />;
if (!auth) return <Navigate to={redirectLink(Links.Authenticate)} />;
if (manageProjects) return <Navigate to={redirectLink(Links.ProjectView)} />;
return <Navigate to={redirectLink(Links.Authorized)} />;
}
export function RedirectLoader() {
@ -33,3 +40,30 @@ export function RedirectLoader() {
</Center>
);
}
export function RedirectPortal() {
const { auth, token } = useAuth();
const [search] = useSearchParams();
const redirectUri = search.get("redirectUri");
useEffect(() => {
if (!auth || !redirectUri || !token) return;
window.location.href = `${redirectUri}?cairoUserToken=${token}`;
}, [auth, redirectUri, token]);
if (!auth) return <Navigate to={redirectLink(Links.Authenticate)} />;
return (
<Flex h="100vh" w="100%">
<Center w="100%">
<Flex flexWrap="wrap" textAlign="center">
<Text w="100%" fontSize="16px">
Your request has been authorized!
</Text>
<Text w="100%" fontSize="13px">
You will be automatically redirected within a few seconds
</Text>
</Flex>
</Center>
</Flex>
);
}