[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
|
- name: Oasis Setup
|
||||||
uses: https://forgejo.dunemask.dev/elysium/elysium-actions@oasis-setup-auto
|
uses: https://forgejo.dunemask.dev/elysium/elysium-actions@oasis-setup-auto
|
||||||
with:
|
with:
|
||||||
deploy-env: edge
|
deploy-env: prod
|
||||||
infisical-token: ${{ secrets.INFISICAL_ELYSIUM_EDGE_READ_TOKEN }}
|
infisical-token: ${{ secrets.INFISICAL_ELYSIUM_EDGE_READ_TOKEN }}
|
||||||
extra-secret-paths: /dashboard,/alexandria
|
extra-secret-paths: /dashboard,/devops,/kubernetes
|
||||||
extra-secret-envs: edge,edge
|
extra-secret-envs: prod,prod,prod
|
||||||
# Deploy to Edge
|
# Deploy to Edge
|
||||||
- name: Deploy to Edge env
|
- name: Deploy to Edge env
|
||||||
run: garden deploy $GARDEN_DEPLOY_ACTION --force --force-build --env usw-edge
|
run: garden deploy $GARDEN_DEPLOY_ACTION --force --force-build --env usw-edge
|
||||||
|
|
|
@ -26,3 +26,6 @@ src/
|
||||||
build/
|
build/
|
||||||
public/
|
public/
|
||||||
lib/
|
lib/
|
||||||
|
prisma/
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
FROM node:20-bookworm-slim
|
FROM node:20-bookworm-slim
|
||||||
WORKDIR /dunemask/net/cairo
|
WORKDIR /dunemask/net/cairo
|
||||||
|
RUN apt-get update -y && apt-get install -y openssl
|
||||||
COPY package.json .
|
COPY package.json .
|
||||||
COPY package-lock.json .
|
COPY package-lock.json .
|
||||||
COPY .npmrc .
|
COPY .npmrc .
|
||||||
|
|
|
@ -36,13 +36,13 @@ export default class AuthController extends VixpressController {
|
||||||
if (projectKeyPairs.length !== 1) throw ProjectErrors.BadRequestProjectIncomplete;
|
if (projectKeyPairs.length !== 1) throw ProjectErrors.BadRequestProjectIncomplete;
|
||||||
const token = await getUserToken(user.id, user.project.keyPairs[0].encryptedPrivateKey);
|
const token = await getUserToken(user.id, user.project.keyPairs[0].encryptedPrivateKey);
|
||||||
const policies = user.rolePolicy.policies;
|
const policies = user.rolePolicy.policies;
|
||||||
const userData: CDatabaseContract["User"] = { username: user.username, rolePolicyId: user.rolePolicyId };
|
const usr: CDatabaseContract["User"] = { id: user.id, username: user.username, rolePolicyId: user.rolePolicyId };
|
||||||
return { token, user: userData, policies };
|
return { token, user: usr, policies };
|
||||||
}
|
}
|
||||||
|
|
||||||
async credentials(crc: ContractRouteContext): Promise<CAuthContract["Credentials"]> {
|
async credentials(crc: ContractRouteContext): Promise<CAuthContract["Credentials"]> {
|
||||||
const { user, policies } = crc.req as UserRequest;
|
const { user, policies } = crc.req as UserRequest;
|
||||||
const userData: CDatabaseContract["User"] = { username: user.username, rolePolicyId: user.rolePolicyId };
|
const usr: CDatabaseContract["User"] = { id: user.id, username: user.username, rolePolicyId: user.rolePolicyId };
|
||||||
return { user: userData, policies: ResourcePolicy.asStrings(policies) };
|
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);
|
const kp = await this.pg.keypair.byUsage(proj.id, KeyPairType.UserToken);
|
||||||
if (!kp) throw ProjectErrors.BadRequestProjectIncomplete;
|
if (!kp) throw ProjectErrors.BadRequestProjectIncomplete;
|
||||||
if (!user) throw ProjectErrors.UnexpectedRootUserError;
|
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);
|
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", {
|
export const DatabaseContractRes = defineContractExport("CDatabaseContractRes", {
|
||||||
User: y.object({
|
User: y.object({
|
||||||
|
id: y.string().required(),
|
||||||
username: y.string().required(),
|
username: y.string().required(),
|
||||||
email: y.string().nullable(),
|
email: y.string().nullable(),
|
||||||
hash: antiRequired,
|
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 { 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 { CenteredLoadingSpinner } from "./components/common/Loading";
|
||||||
import { CenteredErrorFallback } from "./components/common/Fallback";
|
import { CenteredErrorFallback } from "./components/common/Fallback";
|
||||||
import { Links, rootLink } from "./util/links";
|
import { Links, rootLink } from "./util/links";
|
||||||
|
@ -17,9 +17,9 @@ import AuthorizedView from "./views/AuthorizedView";
|
||||||
declare type Portal = { path: Links; view: ReactNode };
|
declare type Portal = { path: Links; view: ReactNode };
|
||||||
|
|
||||||
function Auth(props: { view: (() => ReactNode) | LazyExoticComponent<() => ReactNode> }) {
|
function Auth(props: { view: (() => ReactNode) | LazyExoticComponent<() => ReactNode> }) {
|
||||||
const { auth } = useAuth();
|
const { auth, loading, initialized } = useAuth();
|
||||||
const Component = props.view;
|
const Component = props.view;
|
||||||
if (!!auth) return <Component />;
|
if (!loading && !initialized && !!auth) return <Component />;
|
||||||
return <AutoRedirect />;
|
return <AutoRedirect />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,10 @@ import { apiRequest } from "@dunemask/vix/bridge";
|
||||||
import { Policy, PolicyString } from "@lib/Policies";
|
import { Policy, PolicyString } from "@lib/Policies";
|
||||||
import { Resource } from "@lib/vix/AppResources";
|
import { Resource } from "@lib/vix/AppResources";
|
||||||
import { CDatabaseContract } from "@lib/contracts/database.contracts";
|
import { CDatabaseContract } from "@lib/contracts/database.contracts";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
const project = import.meta.env.VITE_CAIRO_PROJECT as string;
|
const project = import.meta.env.VITE_CAIRO_PROJECT as string;
|
||||||
const credentialApiPath = `/${project}/auth/credentials`;
|
const credentialApiPath = `auth/credentials`;
|
||||||
|
|
||||||
export enum AuthStorageKeys {
|
export enum AuthStorageKeys {
|
||||||
USER = "user",
|
USER = "user",
|
||||||
|
@ -147,16 +148,19 @@ function authReducer(state: AuthState, action: Action): AuthState {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPolicies(token: string): Promise<Policy[]> {
|
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 extraHeaders = { Authorization: `Bearer ${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) return [];
|
if (!credentials) return [];
|
||||||
const { policies } = credentials as CredentialDTO;
|
const { policies } = credentials as CredentialDTO;
|
||||||
return Policy.parseResourcePolicies<Resource>(policies);
|
return Policy.parseResourcePolicies<Resource>(policies);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|
||||||
const [state, dispatch] = useReducer(authReducer, initialState);
|
|
||||||
|
|
||||||
async function login(userData: CDatabaseContract["User"], token: string, policies?: Policy[]) {
|
async function login(userData: CDatabaseContract["User"], token: string, policies?: Policy[]) {
|
||||||
dispatch({ type: AuthAction.LOADING, loading: true });
|
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[]]> {
|
async function fetchCredentials(token: string): Promise<[user: CDatabaseContract["User"], rp: Policy[]]> {
|
||||||
const extraHeaders = getAuthHeader(token);
|
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!");
|
if (!credentials) throw Error("Could not authenticate!");
|
||||||
const { user, policies } = credentials as CredentialDTO;
|
const { user, policies } = credentials as CredentialDTO;
|
||||||
if (!user || !policies) throw Error("Could not authenticate!");
|
if (!user || !policies) throw Error("Could not authenticate!");
|
||||||
|
|
|
@ -21,6 +21,8 @@ export function useLinkNav() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAutoRedirect() {
|
export function useAutoRedirect() {
|
||||||
const nav = useLinkNav();
|
const search = window.location.search;
|
||||||
return () => nav(Links.AutoRedirect);
|
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 { SyntheticEvent, useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { useAuth } from "@src/ctx/AuthContext";
|
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 { ClientError } from "@dunemask/vix/bridge";
|
||||||
import { AuthErrors } from "@lib/vix/ClientErrors";
|
import { AuthErrors } from "@lib/vix/ClientErrors";
|
||||||
import { PasswordInput } from "@src/components/common/Inputs";
|
import { PasswordInput } from "@src/components/common/Inputs";
|
||||||
import { ResourcePolicyType } from "@dunemask/vix/util";
|
import { ResourcePolicyType } from "@dunemask/vix/util";
|
||||||
import { postProjectAuthLogin } from "@src/util/api/GeneratedRequests";
|
import { postProjectAuthLogin } from "@src/util/api/GeneratedRequests";
|
||||||
|
import { Navigate, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
const project = import.meta.env.VITE_CAIRO_PROJECT as string;
|
const project = import.meta.env.VITE_CAIRO_PROJECT as string;
|
||||||
|
|
||||||
export default function AuthenticateView() {
|
export default function AuthenticateView() {
|
||||||
const { auth, login } = useAuth();
|
const { auth, login } = useAuth();
|
||||||
const autoRedirect = useAutoRedirect();
|
const autoRedirect = useAutoRedirect();
|
||||||
|
const [search] = useSearchParams();
|
||||||
const [identity, setIdentity] = useState<string>("");
|
const [identity, setIdentity] = useState<string>("");
|
||||||
const [password, setPassword] = useState<string>("");
|
const [password, setPassword] = useState<string>("");
|
||||||
|
const projectId = search.get("projectId") ?? project;
|
||||||
|
|
||||||
const identityChange = (e: SyntheticEvent) => setIdentity((e.target as HTMLInputElement).value);
|
const identityChange = (e: SyntheticEvent) => setIdentity((e.target as HTMLInputElement).value);
|
||||||
|
|
||||||
function submitCredentials() {
|
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!");
|
if (!creds.token) return toast.error("Server didn't provide token!");
|
||||||
await login(
|
await login(
|
||||||
creds.user,
|
creds.user,
|
||||||
|
@ -48,7 +52,7 @@ export default function AuthenticateView() {
|
||||||
function detectEnter(e: KeyboardEvent) {
|
function detectEnter(e: KeyboardEvent) {
|
||||||
if (e.key === "Enter") submitCredentials();
|
if (e.key === "Enter") submitCredentials();
|
||||||
}
|
}
|
||||||
if (auth) return;
|
if (auth) return <Navigate to={rootLink(Links.AutoRedirect)} />;
|
||||||
return (
|
return (
|
||||||
<Box width="100%" height="90vh" display="flex" alignItems="center" justifyContent="center">
|
<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">
|
<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 useContentGuard, { GuardedContent } from "@src/util/guards";
|
||||||
import { Links, rootLink } from "@src/util/links";
|
import { Links, rootLink } from "@src/util/links";
|
||||||
import { useEffect, useState } from "react";
|
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() {
|
export default function AutoRedirect() {
|
||||||
const manageProjects = useContentGuard(AppGuard.ManageProjects);
|
const manageProjects = useContentGuard(AppGuard.ManageProjects);
|
||||||
|
const [serachParams] = useSearchParams();
|
||||||
|
const redirectUri = serachParams.get("redirectUri");
|
||||||
|
|
||||||
const { auth, initialized, loading } = useAuth();
|
const { auth, initialized, loading } = useAuth();
|
||||||
if (!initialized || loading) return <RedirectLoader />;
|
if (!initialized || loading) return <RedirectLoader />;
|
||||||
if (!auth) return <Navigate to={rootLink(Links.Authenticate)} />;
|
if (!!redirectUri) return <RedirectPortal />;
|
||||||
if (manageProjects) return <Navigate to={rootLink(Links.ProjectView)} />;
|
if (!auth) return <Navigate to={redirectLink(Links.Authenticate)} />;
|
||||||
return <Navigate to={rootLink(Links.Authorized)} />;
|
if (manageProjects) return <Navigate to={redirectLink(Links.ProjectView)} />;
|
||||||
|
return <Navigate to={redirectLink(Links.Authorized)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RedirectLoader() {
|
export function RedirectLoader() {
|
||||||
|
@ -33,3 +40,30 @@ export function RedirectLoader() {
|
||||||
</Center>
|
</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