From fdb19be2efdaaeb047e567a56c8b1130d2a6afb1 Mon Sep 17 00:00:00 2001 From: Dunemask Date: Sat, 24 Aug 2024 16:51:30 -0600 Subject: [PATCH] [CHORE] Implement RedirectURIs --- .dockerignore | 9 ++++- .forgejo/workflows/deploy-edge.yml | 6 ++-- .helmignore | 3 ++ Dockerfile | 1 + lib/modules/auth/auth.controller.ts | 8 ++--- lib/modules/projects/project.controller.ts | 4 +-- lib/types/contracts/database.contracts.ts | 1 + src/Portal.tsx | 8 ++--- src/ctx/AuthContext.tsx | 26 ++++++++------ src/util/links.ts | 6 ++-- src/views/AuthenticateView.tsx | 10 ++++-- src/views/AutoRedirect.tsx | 42 +++++++++++++++++++--- 12 files changed, 91 insertions(+), 33 deletions(-) diff --git a/.dockerignore b/.dockerignore index 40b878d..869b4d2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,8 @@ -node_modules/ \ No newline at end of file +node_modules/ +templates/ +dist/ +.helmignore +Chart.yaml +values.yaml +.forgejo/ +.git/ \ No newline at end of file diff --git a/.forgejo/workflows/deploy-edge.yml b/.forgejo/workflows/deploy-edge.yml index 0b460ef..fe3f9a8 100644 --- a/.forgejo/workflows/deploy-edge.yml +++ b/.forgejo/workflows/deploy-edge.yml @@ -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 diff --git a/.helmignore b/.helmignore index 0a8009a..f8b3217 100644 --- a/.helmignore +++ b/.helmignore @@ -26,3 +26,6 @@ src/ build/ public/ lib/ +prisma/ +package.json +package-lock.json diff --git a/Dockerfile b/Dockerfile index 522cde4..592ad0d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 . diff --git a/lib/modules/auth/auth.controller.ts b/lib/modules/auth/auth.controller.ts index a9640a2..10f5e49 100644 --- a/lib/modules/auth/auth.controller.ts +++ b/lib/modules/auth/auth.controller.ts @@ -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 { 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) }; } } diff --git a/lib/modules/projects/project.controller.ts b/lib/modules/projects/project.controller.ts index 73ff55e..5cd57cc 100644 --- a/lib/modules/projects/project.controller.ts +++ b/lib/modules/projects/project.controller.ts @@ -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 }; } } diff --git a/lib/types/contracts/database.contracts.ts b/lib/types/contracts/database.contracts.ts index 99bf043..6befd1c 100644 --- a/lib/types/contracts/database.contracts.ts +++ b/lib/types/contracts/database.contracts.ts @@ -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, diff --git a/src/Portal.tsx b/src/Portal.tsx index 271cec9..514254c 100644 --- a/src/Portal.tsx +++ b/src/Portal.tsx @@ -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 ; + if (!loading && !initialized && !!auth) return ; return ; } diff --git a/src/ctx/AuthContext.tsx b/src/ctx/AuthContext.tsx index b867f9a..cc8dc11 100644 --- a/src/ctx/AuthContext.tsx +++ b/src/ctx/AuthContext.tsx @@ -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 { - 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(policies); -} - export const AuthProvider = ({ children }: { children: ReactNode }) => { const [state, dispatch] = useReducer(authReducer, initialState); + const [search] = useSearchParams(); + + async function getPolicies(token: string): Promise { + 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(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!"); diff --git a/src/util/links.ts b/src/util/links.ts index c7f83fb..f7b89ae 100644 --- a/src/util/links.ts +++ b/src/util/links.ts @@ -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); } diff --git a/src/views/AuthenticateView.tsx b/src/views/AuthenticateView.tsx index 36c9046..3cf3425 100644 --- a/src/views/AuthenticateView.tsx +++ b/src/views/AuthenticateView.tsx @@ -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(""); const [password, setPassword] = useState(""); + 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 ; return ( diff --git a/src/views/AutoRedirect.tsx b/src/views/AutoRedirect.tsx index 30083a6..5f0fba7 100644 --- a/src/views/AutoRedirect.tsx +++ b/src/views/AutoRedirect.tsx @@ -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 ; - if (!auth) return ; - if (manageProjects) return ; - return ; + if (!!redirectUri) return ; + if (!auth) return ; + if (manageProjects) return ; + return ; } export function RedirectLoader() { @@ -33,3 +40,30 @@ export function RedirectLoader() { ); } + +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 ; + + return ( + +
+ + + Your request has been authorized! + + + You will be automatically redirected within a few seconds + + +
+
+ ); +}