[CHORE] Implement RedirectURIs
This commit is contained in:
parent
0fc5f05b6a
commit
fdb19be2ef
12 changed files with 91 additions and 33 deletions
|
@ -1 +1,8 @@
|
|||
node_modules/
|
||||
node_modules/
|
||||
templates/
|
||||
dist/
|
||||
.helmignore
|
||||
Chart.yaml
|
||||
values.yaml
|
||||
.forgejo/
|
||||
.git/
|
|
@ -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
|
||||
|
|
|
@ -26,3 +26,6 @@ src/
|
|||
build/
|
||||
public/
|
||||
lib/
|
||||
prisma/
|
||||
package.json
|
||||
package-lock.json
|
||||
|
|
|
@ -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 .
|
||||
|
|
|
@ -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) };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 />;
|
||||
}
|
||||
|
||||
|
|
|
@ -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!");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue