@@ -8,7 +8,10 @@ import { BrowserRouter, Route, Routes, useLocation } from "react-router";
|
|||||||
import "App.css";
|
import "App.css";
|
||||||
import ErrorBoundary from "./components/ErrorBoundary";
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import ConsoleLayout from "./layouts/ConsoleLayout";
|
import ConsoleLayout from "./layouts/ConsoleLayout";
|
||||||
|
import AuthLayout from "./layouts/AuthLayout";
|
||||||
import { RelayEnvironment } from "./RelayEnvironment";
|
import { RelayEnvironment } from "./RelayEnvironment";
|
||||||
|
import { AuthProvider } from "./contexts/AuthContext";
|
||||||
|
import { ProtectedRoute } from "./components/ProtectedRoute";
|
||||||
|
|
||||||
posthog.init(process.env.POSTHOG_KEY!, {
|
posthog.init(process.env.POSTHOG_KEY!, {
|
||||||
api_host: process.env.POSTHOG_HOST,
|
api_host: process.env.POSTHOG_HOST,
|
||||||
@@ -29,11 +32,12 @@ const VendorOverviewPage = lazy(() => import("./pages/VendorOverviewPage"));
|
|||||||
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
|
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
|
||||||
const CreatePeoplePage = lazy(() => import("./pages/CreatePeoplePage"));
|
const CreatePeoplePage = lazy(() => import("./pages/CreatePeoplePage"));
|
||||||
const FrameworkOverviewPage = lazy(
|
const FrameworkOverviewPage = lazy(
|
||||||
() => import("./pages/FrameworkOverviewPage"),
|
() => import("./pages/FrameworkOverviewPage")
|
||||||
);
|
);
|
||||||
const ControlOverviewPage = lazy(() => import("./pages/ControlOverviewPage"));
|
const ControlOverviewPage = lazy(() => import("./pages/ControlOverviewPage"));
|
||||||
const PeopleOverviewPage = lazy(() => import("./pages/PeopleOverviewPage"));
|
const PeopleOverviewPage = lazy(() => import("./pages/PeopleOverviewPage"));
|
||||||
|
const LoginPage = lazy(() => import("./pages/LoginPage"));
|
||||||
|
const RegisterPage = lazy(() => import("./pages/RegisterPage"));
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -43,125 +47,171 @@ function App() {
|
|||||||
<RelayEnvironmentProvider environment={RelayEnvironment}>
|
<RelayEnvironmentProvider environment={RelayEnvironment}>
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<AuthProvider>
|
||||||
<Route
|
<Routes>
|
||||||
path="/*"
|
{/* Authentication Routes - Accessible without login */}
|
||||||
element={
|
|
||||||
<ErrorBoundaryWithLocation>
|
|
||||||
<ConsoleLayout />
|
|
||||||
</ErrorBoundaryWithLocation>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Route
|
<Route
|
||||||
index
|
path="/login"
|
||||||
element={
|
element={
|
||||||
<Suspense>
|
<ErrorBoundaryWithLocation>
|
||||||
|
<AuthLayout />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Route
|
||||||
|
index
|
||||||
|
element={
|
||||||
|
<Suspense>
|
||||||
|
<ErrorBoundaryWithLocation>
|
||||||
|
<LoginPage />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/register"
|
||||||
|
element={
|
||||||
|
<ErrorBoundaryWithLocation>
|
||||||
|
<AuthLayout />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Route
|
||||||
|
index
|
||||||
|
element={
|
||||||
|
<Suspense>
|
||||||
|
<ErrorBoundaryWithLocation>
|
||||||
|
<RegisterPage />
|
||||||
|
</ErrorBoundaryWithLocation>
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* Protected Routes - Require authentication */}
|
||||||
|
<Route element={<ProtectedRoute />}>
|
||||||
|
<Route
|
||||||
|
path="/*"
|
||||||
|
element={
|
||||||
<ErrorBoundaryWithLocation>
|
<ErrorBoundaryWithLocation>
|
||||||
<HomePage />
|
<ConsoleLayout />
|
||||||
</ErrorBoundaryWithLocation>
|
</ErrorBoundaryWithLocation>
|
||||||
</Suspense>
|
}
|
||||||
}
|
>
|
||||||
/>
|
<Route
|
||||||
<Route
|
index
|
||||||
path="peoples"
|
element={
|
||||||
element={
|
<Suspense>
|
||||||
<Suspense>
|
<ErrorBoundaryWithLocation>
|
||||||
<ErrorBoundaryWithLocation>
|
<HomePage />
|
||||||
<PeopleListPage />
|
</ErrorBoundaryWithLocation>
|
||||||
</ErrorBoundaryWithLocation>
|
</Suspense>
|
||||||
</Suspense>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="peoples"
|
||||||
path="peoples/create"
|
element={
|
||||||
element={
|
<Suspense>
|
||||||
<Suspense>
|
<ErrorBoundaryWithLocation>
|
||||||
<ErrorBoundaryWithLocation>
|
<PeopleListPage />
|
||||||
<CreatePeoplePage />
|
</ErrorBoundaryWithLocation>
|
||||||
</ErrorBoundaryWithLocation>
|
</Suspense>
|
||||||
</Suspense>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="peoples/create"
|
||||||
path="peoples/:peopleId"
|
element={
|
||||||
element={
|
<Suspense>
|
||||||
<Suspense>
|
<ErrorBoundaryWithLocation>
|
||||||
<ErrorBoundaryWithLocation>
|
<CreatePeoplePage />
|
||||||
<PeopleOverviewPage />
|
</ErrorBoundaryWithLocation>
|
||||||
</ErrorBoundaryWithLocation>
|
</Suspense>
|
||||||
</Suspense>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="peoples/:peopleId"
|
||||||
path="vendors"
|
element={
|
||||||
element={
|
<Suspense>
|
||||||
<Suspense>
|
<ErrorBoundaryWithLocation>
|
||||||
<ErrorBoundaryWithLocation>
|
<PeopleOverviewPage />
|
||||||
<VendorListPage />
|
</ErrorBoundaryWithLocation>
|
||||||
</ErrorBoundaryWithLocation>
|
</Suspense>
|
||||||
</Suspense>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="vendors"
|
||||||
path="frameworks"
|
element={
|
||||||
element={
|
<Suspense>
|
||||||
<Suspense>
|
<ErrorBoundaryWithLocation>
|
||||||
<ErrorBoundaryWithLocation>
|
<VendorListPage />
|
||||||
<FrameworkListPage />
|
</ErrorBoundaryWithLocation>
|
||||||
</ErrorBoundaryWithLocation>
|
</Suspense>
|
||||||
</Suspense>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="frameworks"
|
||||||
path="frameworks/:frameworkId"
|
element={
|
||||||
element={
|
<Suspense>
|
||||||
<Suspense>
|
<ErrorBoundaryWithLocation>
|
||||||
<ErrorBoundaryWithLocation>
|
<FrameworkListPage />
|
||||||
<FrameworkOverviewPage />
|
</ErrorBoundaryWithLocation>
|
||||||
</ErrorBoundaryWithLocation>
|
</Suspense>
|
||||||
</Suspense>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="frameworks/:frameworkId"
|
||||||
path="frameworks/:frameworkId/controls/:controlId"
|
element={
|
||||||
element={
|
<Suspense>
|
||||||
<Suspense>
|
<ErrorBoundaryWithLocation>
|
||||||
<ErrorBoundaryWithLocation>
|
<FrameworkOverviewPage />
|
||||||
<ControlOverviewPage />
|
</ErrorBoundaryWithLocation>
|
||||||
</ErrorBoundaryWithLocation>
|
</Suspense>
|
||||||
</Suspense>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="frameworks/:frameworkId/controls/:controlId"
|
||||||
path="vendors/:vendorId"
|
element={
|
||||||
element={
|
<Suspense>
|
||||||
<Suspense>
|
<ErrorBoundaryWithLocation>
|
||||||
<ErrorBoundaryWithLocation>
|
<ControlOverviewPage />
|
||||||
<VendorOverviewPage />
|
</ErrorBoundaryWithLocation>
|
||||||
</ErrorBoundaryWithLocation>
|
</Suspense>
|
||||||
</Suspense>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="vendors/:vendorId"
|
||||||
path="settings"
|
element={
|
||||||
element={
|
<Suspense>
|
||||||
<Suspense>
|
<ErrorBoundaryWithLocation>
|
||||||
<ErrorBoundaryWithLocation>
|
<VendorOverviewPage />
|
||||||
<SettingsPage />
|
</ErrorBoundaryWithLocation>
|
||||||
</ErrorBoundaryWithLocation>
|
</Suspense>
|
||||||
</Suspense>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="settings"
|
||||||
path="*"
|
element={
|
||||||
element={
|
<Suspense>
|
||||||
<Suspense>
|
<ErrorBoundaryWithLocation>
|
||||||
<NotFoundPage />
|
<SettingsPage />
|
||||||
</Suspense>
|
</ErrorBoundaryWithLocation>
|
||||||
}
|
</Suspense>
|
||||||
/>
|
}
|
||||||
</Route>
|
/>
|
||||||
</Routes>
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={
|
||||||
|
<Suspense>
|
||||||
|
<NotFoundPage />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</AuthProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
</RelayEnvironmentProvider>
|
</RelayEnvironmentProvider>
|
||||||
|
|||||||
20
apps/console/src/components/ProtectedRoute.tsx
Normal file
20
apps/console/src/components/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Navigate, Outlet, useLocation } from "react-router";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
|
||||||
|
export function ProtectedRoute() {
|
||||||
|
const { isAuthenticated, isLoading } = useAuth();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
// Show nothing while checking authentication
|
||||||
|
if (isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to login if not authenticated
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to="/login" replace state={{ from: location }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the protected content
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<578d66fa0d0065452d9fa64611b7cfda>>
|
* @generated SignedSource<<717a5c6c939c65b7e0790f84e2c41bcd>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -72,7 +72,7 @@ return {
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "Viewer",
|
"concreteType": "User",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
@@ -109,7 +109,7 @@ return {
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "Viewer",
|
"concreteType": "User",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
|
|||||||
78
apps/console/src/contexts/AuthContext.tsx
Normal file
78
apps/console/src/contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import { buildEndpoint } from "@/utils";
|
||||||
|
|
||||||
|
interface AuthContextType {
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
checkAuth: () => Promise<boolean>;
|
||||||
|
logout: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const checkAuth = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
// Make a request to an endpoint that requires authentication
|
||||||
|
const response = await fetch(buildEndpoint("/console/v1/query"), {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
operationName: "CheckAuth",
|
||||||
|
query: `query CheckAuth { viewer { id } }`,
|
||||||
|
variables: {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const authenticated =
|
||||||
|
response.ok && !response.headers.get("WWW-Authenticate");
|
||||||
|
setIsAuthenticated(authenticated);
|
||||||
|
return authenticated;
|
||||||
|
} catch (error) {
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await fetch(buildEndpoint("/console/v1/auth/logout"), {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
navigate("/login");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkAuth();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{ isAuthenticated, isLoading, checkAuth, logout }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuth(): AuthContextType {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useAuth must be used within an AuthProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
40
apps/console/src/layouts/AuthLayout.tsx
Normal file
40
apps/console/src/layouts/AuthLayout.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Outlet } from "react-router";
|
||||||
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
|
||||||
|
export default function AuthLayout() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen flex-col">
|
||||||
|
<div className="flex flex-1">
|
||||||
|
<div className="flex flex-1 flex-col bg-muted/40">
|
||||||
|
<div className="flex flex-1 items-center justify-center">
|
||||||
|
<main className="w-full max-w-md p-6">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="hidden flex-1 bg-gradient-to-br from-gray-900 to-gray-950 lg:block">
|
||||||
|
<div className="flex h-full items-center justify-center p-8">
|
||||||
|
<div className="relative h-full w-full">
|
||||||
|
<div className="absolute right-0 top-1/4 z-10">
|
||||||
|
<img
|
||||||
|
src="/assets/android-chrome-512x512.png"
|
||||||
|
alt="Probo Mascot"
|
||||||
|
className="h-auto w-96"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute left-8 top-1/3 z-0 text-white">
|
||||||
|
<h1 className="text-4xl font-bold leading-tight">
|
||||||
|
Navigate compliance with
|
||||||
|
<br />
|
||||||
|
confidence thanks to{" "}
|
||||||
|
<span className="text-lime-400">probo</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Toaster />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
153
apps/console/src/pages/LoginPage.tsx
Normal file
153
apps/console/src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Link, useLocation, useNavigate } from "react-router";
|
||||||
|
import { Helmet } from "react-helmet-async";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { buildEndpoint } from "@/utils";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const { checkAuth } = useAuth();
|
||||||
|
|
||||||
|
// Get the redirect path from location state or default to home
|
||||||
|
const from = location.state?.from?.pathname || "/";
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Email and password are required",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(buildEndpoint("/console/v1/auth/login"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.message || "Login failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check authentication status after login
|
||||||
|
await checkAuth();
|
||||||
|
|
||||||
|
// Redirect to the original page or home after successful login
|
||||||
|
navigate(from, { replace: true });
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error instanceof Error ? error.message : "Login failed",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>Log in - Probo</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-2 text-center">
|
||||||
|
<h1 className="text-3xl font-bold">Log in</h1>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
Enter your credentials to access your account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">Email</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="name@example.com"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password">Password</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||||
|
{isLoading ? "Logging in..." : "Log in with email"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Don't have an account?{" "}
|
||||||
|
<Link to="/register" className="underline">
|
||||||
|
Sign up here
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<span className="w-full border-t" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-xs uppercase">
|
||||||
|
<span className="bg-background px-2 text-muted-foreground">
|
||||||
|
Or continue with
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="outline" className="w-full" disabled={isLoading}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="mr-2 h-4 w-4"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M17.13 17.13v-4.26l-3.2 3.2a4.33 4.33 0 0 1-6.13-6.13l3.2-3.2h-4.26" />
|
||||||
|
</svg>
|
||||||
|
Log in with Google
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
155
apps/console/src/pages/RegisterPage.tsx
Normal file
155
apps/console/src/pages/RegisterPage.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Link, useNavigate } from "react-router";
|
||||||
|
import { Helmet } from "react-helmet-async";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { buildEndpoint } from "@/utils";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { checkAuth } = useAuth();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Email and password are required",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(buildEndpoint("/console/v1/auth/register"), {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.message || "Registration failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Account created successfully",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check authentication status after registration
|
||||||
|
await checkAuth();
|
||||||
|
|
||||||
|
// Redirect to home page after successful registration
|
||||||
|
navigate("/");
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description:
|
||||||
|
error instanceof Error ? error.message : "Registration failed",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>Sign up - Probo</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-2 text-center">
|
||||||
|
<h1 className="text-3xl font-bold">Sign up</h1>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
Enter your information to create an account
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">Email</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="name@example.com"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password">Password</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||||
|
{isLoading ? "Creating account..." : "Sign up with email"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Already have an account?{" "}
|
||||||
|
<Link to="/login" className="underline">
|
||||||
|
Log in here
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<span className="w-full border-t" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-xs uppercase">
|
||||||
|
<span className="bg-background px-2 text-muted-foreground">
|
||||||
|
Or continue with
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="outline" className="w-full" disabled={isLoading}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="mr-2 h-4 w-4"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M17.13 17.13v-4.26l-3.2 3.2a4.33 4.33 0 0 1-6.13-6.13l3.2-3.2h-4.26" />
|
||||||
|
</svg>
|
||||||
|
Sign up with Google
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<25866c1f4e6fb640f91b8d05651df9f4>>
|
* @generated SignedSource<<bcc0482212bee6fc9682545cc8255139>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -36,7 +36,7 @@ v1 = [
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "Viewer",
|
"concreteType": "User",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<b79fe4cf20a9bc930e7bd90ccc2daf03>>
|
* @generated SignedSource<<903f19e5b3c193151107de5969fe5259>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -163,7 +163,7 @@ return {
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "Viewer",
|
"concreteType": "User",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
@@ -197,7 +197,7 @@ return {
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "Viewer",
|
"concreteType": "User",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<d57e3b87db862a1cfa086ea4b410d567>>
|
* @generated SignedSource<<7ca2f2186b657853d9fce4c7a21a7db1>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -615,7 +615,7 @@ return {
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "Viewer",
|
"concreteType": "User",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
@@ -654,7 +654,7 @@ return {
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "Viewer",
|
"concreteType": "User",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<dde370798e95a61988dd69cdde3acaad>>
|
* @generated SignedSource<<fd54290261cf29d9b3cf42b70a68f398>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -94,7 +94,7 @@ return {
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "Viewer",
|
"concreteType": "User",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
@@ -137,7 +137,7 @@ return {
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "Viewer",
|
"concreteType": "User",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<fab1b817b8d6876b5bf4dc9320c4deef>>
|
* @generated SignedSource<<0b5e0bcf4f6519e1ccb1d7ffa3e488cb>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -56,7 +56,7 @@ return {
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "Viewer",
|
"concreteType": "User",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
@@ -91,7 +91,7 @@ return {
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "Viewer",
|
"concreteType": "User",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<4b2d9a3c1a701053fe929da59580308b>>
|
* @generated SignedSource<<42f5a79d59c05d56dc664163fd507fed>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -94,7 +94,7 @@ return {
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "Viewer",
|
"concreteType": "User",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
@@ -137,7 +137,7 @@ return {
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "Viewer",
|
"concreteType": "User",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type (
|
|||||||
AllowedOrigins []string
|
AllowedOrigins []string
|
||||||
Probo *probo.Service
|
Probo *probo.Service
|
||||||
Usrmgr *usrmgr.Service
|
Usrmgr *usrmgr.Service
|
||||||
|
Auth console_v1.AuthConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
Server struct {
|
Server struct {
|
||||||
@@ -39,7 +40,8 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrMissingProboService = errors.New("server configuration requires a valid probo.Service instance")
|
ErrMissingProboService = errors.New("server configuration requires a valid probo.Service instance")
|
||||||
|
ErrMissingUsrmgrService = errors.New("server configuration requires a valid usrmgr.Service instance")
|
||||||
)
|
)
|
||||||
|
|
||||||
func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
|
func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -71,6 +73,10 @@ func NewServer(cfg Config) (*Server, error) {
|
|||||||
return nil, ErrMissingProboService
|
return nil, ErrMissingProboService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.Usrmgr == nil {
|
||||||
|
return nil, ErrMissingUsrmgrService
|
||||||
|
}
|
||||||
|
|
||||||
return &Server{
|
return &Server{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -94,7 +100,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
router.Use(cors.Handler(corsOpts))
|
router.Use(cors.Handler(corsOpts))
|
||||||
|
|
||||||
router.Mount("/console/v1", console_v1.NewMux(s.cfg.Probo))
|
// Mount the console API with authentication
|
||||||
|
router.Mount("/console/v1", console_v1.NewMux(s.cfg.Probo, s.cfg.Usrmgr, s.cfg.Auth))
|
||||||
|
|
||||||
router.ServeHTTP(w, r)
|
router.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|||||||
203
pkg/api/console/v1/auth_handlers.go
Normal file
203
pkg/api/console/v1/auth_handlers.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
package console_v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/getprobo/probo/pkg/gid"
|
||||||
|
"github.com/getprobo/probo/pkg/usrmgr"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// RegisterRequest represents the request body for user registration
|
||||||
|
RegisterRequest struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginRequest represents the request body for user login
|
||||||
|
LoginRequest struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthResponse represents the response for successful authentication
|
||||||
|
AuthResponse struct {
|
||||||
|
User UserResponse `json:"user"`
|
||||||
|
Session SessionResponse `json:"session"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserResponse represents user data in the authentication response
|
||||||
|
UserResponse struct {
|
||||||
|
ID gid.GID `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionResponse represents session data in the authentication response
|
||||||
|
SessionResponse struct {
|
||||||
|
ID gid.GID `json:"id"`
|
||||||
|
ExpiresAt time.Time `json:"expiresAt"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterHandler handles user registration
|
||||||
|
func RegisterHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse request body
|
||||||
|
var req RegisterRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate request
|
||||||
|
if req.Email == "" || req.Password == "" {
|
||||||
|
http.Error(w, "Email and password are required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the user
|
||||||
|
user, err := usrmgrSvc.RegisterUser(
|
||||||
|
r.Context(),
|
||||||
|
usrmgr.RegisterUserParams{
|
||||||
|
Email: req.Email,
|
||||||
|
Password: req.Password,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to register user: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the user in
|
||||||
|
session, err := usrmgrSvc.Login(r.Context(), req.Email, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to login after registration: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the session cookie
|
||||||
|
setSessionCookie(w, session.ID.String(), authCfg)
|
||||||
|
|
||||||
|
// Return response
|
||||||
|
resp := AuthResponse{
|
||||||
|
User: UserResponse{
|
||||||
|
ID: user.ID,
|
||||||
|
Email: user.EmailAddress,
|
||||||
|
CreatedAt: user.CreatedAt,
|
||||||
|
UpdatedAt: user.UpdatedAt,
|
||||||
|
},
|
||||||
|
Session: SessionResponse{
|
||||||
|
ID: session.ID,
|
||||||
|
ExpiresAt: session.ExpiredAt,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||||
|
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginHandler handles user login
|
||||||
|
func LoginHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse request body
|
||||||
|
var req LoginRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate request
|
||||||
|
if req.Email == "" || req.Password == "" {
|
||||||
|
http.Error(w, "Email and password are required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login the user
|
||||||
|
session, err := usrmgrSvc.Login(r.Context(), req.Email, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to login: %v", err), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the user
|
||||||
|
user, err := usrmgrSvc.GetUserBySession(r.Context(), session.ID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to get user: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the session cookie
|
||||||
|
setSessionCookie(w, session.ID.String(), authCfg)
|
||||||
|
|
||||||
|
// Return response
|
||||||
|
resp := AuthResponse{
|
||||||
|
User: UserResponse{
|
||||||
|
ID: user.ID,
|
||||||
|
Email: user.EmailAddress,
|
||||||
|
CreatedAt: user.CreatedAt,
|
||||||
|
UpdatedAt: user.UpdatedAt,
|
||||||
|
},
|
||||||
|
Session: SessionResponse{
|
||||||
|
ID: session.ID,
|
||||||
|
ExpiresAt: session.ExpiredAt,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||||
|
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogoutHandler handles user logout
|
||||||
|
func LogoutHandler(usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get session from cookie
|
||||||
|
cookie, err := r.Cookie(authCfg.CookieName)
|
||||||
|
if err != nil || cookie.Value == "" {
|
||||||
|
http.Error(w, "No active session", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the session ID
|
||||||
|
sessionID, err := gid.ParseGID(cookie.Value)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Invalid session ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout the user
|
||||||
|
err = usrmgrSvc.Logout(r.Context(), sessionID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Failed to logout: %v", err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the session cookie
|
||||||
|
clearSessionCookie(w, authCfg)
|
||||||
|
|
||||||
|
// Return success response
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterAuthRoutes registers the authentication routes
|
||||||
|
func RegisterAuthRoutes(r chi.Router, usrmgrSvc *usrmgr.Service, authCfg AuthConfig) {
|
||||||
|
r.Post("/auth/register", RegisterHandler(usrmgrSvc, authCfg))
|
||||||
|
r.Post("/auth/login", LoginHandler(usrmgrSvc, authCfg))
|
||||||
|
r.Post("/auth/logout", LogoutHandler(usrmgrSvc, authCfg))
|
||||||
|
}
|
||||||
35
pkg/api/console/v1/cookie.go
Normal file
35
pkg/api/console/v1/cookie.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package console_v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setSessionCookie sets a session cookie in the response
|
||||||
|
func setSessionCookie(w http.ResponseWriter, sessionID string, cfg AuthConfig) {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: cfg.CookieName,
|
||||||
|
Value: sessionID,
|
||||||
|
Path: cfg.CookiePath,
|
||||||
|
Domain: cfg.CookieDomain,
|
||||||
|
Secure: cfg.CookieSecure,
|
||||||
|
HttpOnly: cfg.CookieHTTPOnly,
|
||||||
|
MaxAge: int(cfg.SessionDuration.Seconds()),
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
}
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearSessionCookie clears the session cookie
|
||||||
|
func clearSessionCookie(w http.ResponseWriter, cfg AuthConfig) {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: cfg.CookieName,
|
||||||
|
Value: "",
|
||||||
|
Path: cfg.CookiePath,
|
||||||
|
Domain: cfg.CookieDomain,
|
||||||
|
Secure: cfg.CookieSecure,
|
||||||
|
HttpOnly: cfg.CookieHTTPOnly,
|
||||||
|
MaxAge: -1,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
}
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
}
|
||||||
@@ -17,36 +17,86 @@
|
|||||||
package console_v1
|
package console_v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/99designs/gqlgen/graphql"
|
||||||
"github.com/99designs/gqlgen/graphql/handler"
|
"github.com/99designs/gqlgen/graphql/handler"
|
||||||
"github.com/99designs/gqlgen/graphql/handler/extension"
|
"github.com/99designs/gqlgen/graphql/handler/extension"
|
||||||
"github.com/99designs/gqlgen/graphql/handler/transport"
|
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||||
"github.com/99designs/gqlgen/graphql/playground"
|
"github.com/99designs/gqlgen/graphql/playground"
|
||||||
"github.com/getprobo/probo/pkg/api/console/v1/schema"
|
"github.com/getprobo/probo/pkg/api/console/v1/schema"
|
||||||
|
"github.com/getprobo/probo/pkg/gid"
|
||||||
"github.com/getprobo/probo/pkg/probo"
|
"github.com/getprobo/probo/pkg/probo"
|
||||||
|
"github.com/getprobo/probo/pkg/usrmgr"
|
||||||
|
"github.com/getprobo/probo/pkg/usrmgr/coredata"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
AuthConfig struct {
|
||||||
|
CookieName string
|
||||||
|
CookieSecure bool
|
||||||
|
CookieHTTPOnly bool
|
||||||
|
CookieDomain string
|
||||||
|
CookiePath string
|
||||||
|
SessionDuration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
Resolver struct {
|
Resolver struct {
|
||||||
svc *probo.Service
|
proboSvc *probo.Service
|
||||||
|
usrmgrSvc *usrmgr.Service
|
||||||
|
authCfg AuthConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
contextKey string
|
||||||
|
|
||||||
|
httpContext struct {
|
||||||
|
ResponseWriter http.ResponseWriter
|
||||||
|
Request *http.Request
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewMux(probo *probo.Service) *chi.Mux {
|
const (
|
||||||
|
sessionContextKey contextKey = "session"
|
||||||
|
userContextKey contextKey = "user"
|
||||||
|
httpContextKey contextKey = "http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SessionFromContext retrieves the session from the context
|
||||||
|
func SessionFromContext(ctx context.Context) *coredata.Session {
|
||||||
|
session, _ := ctx.Value(sessionContextKey).(*coredata.Session)
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserFromContext retrieves the user from the context
|
||||||
|
func UserFromContext(ctx context.Context) *coredata.User {
|
||||||
|
user, _ := ctx.Value(userContextKey).(*coredata.User)
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMux(proboSvc *probo.Service, usrmgrSvc *usrmgr.Service, authCfg AuthConfig) *chi.Mux {
|
||||||
r := chi.NewMux()
|
r := chi.NewMux()
|
||||||
|
|
||||||
|
// Register authentication routes
|
||||||
|
RegisterAuthRoutes(r, usrmgrSvc, authCfg)
|
||||||
|
|
||||||
|
// GraphQL playground and query endpoint
|
||||||
r.Get("/", playground.Handler("GraphQL", "/console/v1/query"))
|
r.Get("/", playground.Handler("GraphQL", "/console/v1/query"))
|
||||||
r.Post("/query", graphql(probo))
|
r.Post("/query", graphqlHandler(proboSvc, usrmgrSvc, authCfg))
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func graphql(probo *probo.Service) http.HandlerFunc {
|
func graphqlHandler(proboSvc *probo.Service, usrmgrSvc *usrmgr.Service, authCfg AuthConfig) http.HandlerFunc {
|
||||||
es := schema.NewExecutableSchema(
|
es := schema.NewExecutableSchema(
|
||||||
schema.Config{
|
schema.Config{
|
||||||
Resolvers: &Resolver{
|
Resolvers: &Resolver{
|
||||||
svc: probo,
|
proboSvc: proboSvc,
|
||||||
|
usrmgrSvc: usrmgrSvc,
|
||||||
|
authCfg: authCfg,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -54,7 +104,60 @@ func graphql(probo *probo.Service) http.HandlerFunc {
|
|||||||
srv.AddTransport(transport.POST{})
|
srv.AddTransport(transport.POST{})
|
||||||
srv.Use(extension.Introspection{})
|
srv.Use(extension.Introspection{})
|
||||||
|
|
||||||
|
// Add operation middleware for authentication
|
||||||
|
srv.AroundOperations(func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
|
||||||
|
// Skip authentication for introspection queries
|
||||||
|
if op := graphql.GetOperationContext(ctx); op.OperationName == "IntrospectionQuery" {
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the user from context
|
||||||
|
user := UserFromContext(ctx)
|
||||||
|
if user == nil {
|
||||||
|
return func(ctx context.Context) *graphql.Response {
|
||||||
|
return &graphql.Response{
|
||||||
|
Errors: gqlerror.List{gqlerror.Errorf("authentication required")},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue with the operation
|
||||||
|
return next(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Create HTTP context
|
||||||
|
httpCtx := &httpContext{
|
||||||
|
ResponseWriter: w,
|
||||||
|
Request: r,
|
||||||
|
}
|
||||||
|
ctx := context.WithValue(r.Context(), httpContextKey, httpCtx)
|
||||||
|
|
||||||
|
// Extract session from cookie
|
||||||
|
cookie, err := r.Cookie(authCfg.CookieName)
|
||||||
|
if err == nil && cookie.Value != "" {
|
||||||
|
// Parse the session ID
|
||||||
|
sessionID, err := gid.ParseGID(cookie.Value)
|
||||||
|
if err == nil {
|
||||||
|
// Get the session
|
||||||
|
session, err := usrmgrSvc.GetSession(r.Context(), sessionID)
|
||||||
|
if err == nil {
|
||||||
|
// Add session to context
|
||||||
|
ctx = context.WithValue(ctx, sessionContextKey, session)
|
||||||
|
|
||||||
|
// Get the user
|
||||||
|
user, err := usrmgrSvc.GetUserBySession(r.Context(), sessionID)
|
||||||
|
if err == nil {
|
||||||
|
// Add user to context
|
||||||
|
ctx = context.WithValue(ctx, userContextKey, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the request with the new context
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
srv.ServeHTTP(w, r)
|
srv.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -342,14 +342,25 @@ type EvidenceStateTransition {
|
|||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
# Authentication types
|
||||||
node(id: ID!): Node!
|
type User {
|
||||||
viewer: Viewer!
|
id: ID!
|
||||||
|
email: String!
|
||||||
|
|
||||||
|
organization: Organization! @goField(forceResolver: true)
|
||||||
|
|
||||||
|
createdAt: Datetime!
|
||||||
|
updatedAt: Datetime!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Viewer {
|
type Session {
|
||||||
id: ID!
|
id: ID!
|
||||||
organization: Organization! @goField(forceResolver: true)
|
expiresAt: Datetime!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
node(id: ID!): Node!
|
||||||
|
viewer: User!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ type ResolverRoot interface {
|
|||||||
Organization() OrganizationResolver
|
Organization() OrganizationResolver
|
||||||
Query() QueryResolver
|
Query() QueryResolver
|
||||||
Task() TaskResolver
|
Task() TaskResolver
|
||||||
Viewer() ViewerResolver
|
User() UserResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
type DirectiveRoot struct {
|
type DirectiveRoot struct {
|
||||||
@@ -225,6 +225,11 @@ type ComplexityRoot struct {
|
|||||||
Viewer func(childComplexity int) int
|
Viewer func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Session struct {
|
||||||
|
ExpiresAt func(childComplexity int) int
|
||||||
|
ID func(childComplexity int) int
|
||||||
|
}
|
||||||
|
|
||||||
Task struct {
|
Task struct {
|
||||||
CreatedAt func(childComplexity int) int
|
CreatedAt func(childComplexity int) int
|
||||||
Description func(childComplexity int) int
|
Description func(childComplexity int) int
|
||||||
@@ -265,6 +270,14 @@ type ComplexityRoot struct {
|
|||||||
Node func(childComplexity int) int
|
Node func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
User struct {
|
||||||
|
CreatedAt func(childComplexity int) int
|
||||||
|
Email func(childComplexity int) int
|
||||||
|
ID func(childComplexity int) int
|
||||||
|
Organization func(childComplexity int) int
|
||||||
|
UpdatedAt func(childComplexity int) int
|
||||||
|
}
|
||||||
|
|
||||||
Vendor struct {
|
Vendor struct {
|
||||||
CreatedAt func(childComplexity int) int
|
CreatedAt func(childComplexity int) int
|
||||||
Description func(childComplexity int) int
|
Description func(childComplexity int) int
|
||||||
@@ -290,11 +303,6 @@ type ComplexityRoot struct {
|
|||||||
Cursor func(childComplexity int) int
|
Cursor func(childComplexity int) int
|
||||||
Node func(childComplexity int) int
|
Node func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
Viewer struct {
|
|
||||||
ID func(childComplexity int) int
|
|
||||||
Organization func(childComplexity int) int
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ControlResolver interface {
|
type ControlResolver interface {
|
||||||
@@ -322,14 +330,14 @@ type OrganizationResolver interface {
|
|||||||
}
|
}
|
||||||
type QueryResolver interface {
|
type QueryResolver interface {
|
||||||
Node(ctx context.Context, id gid.GID) (types.Node, error)
|
Node(ctx context.Context, id gid.GID) (types.Node, error)
|
||||||
Viewer(ctx context.Context) (*types.Viewer, error)
|
Viewer(ctx context.Context) (*types.User, error)
|
||||||
}
|
}
|
||||||
type TaskResolver interface {
|
type TaskResolver interface {
|
||||||
StateTransisions(ctx context.Context, obj *types.Task, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskStateTransitionConnection, error)
|
StateTransisions(ctx context.Context, obj *types.Task, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskStateTransitionConnection, error)
|
||||||
Evidences(ctx context.Context, obj *types.Task, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceConnection, error)
|
Evidences(ctx context.Context, obj *types.Task, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceConnection, error)
|
||||||
}
|
}
|
||||||
type ViewerResolver interface {
|
type UserResolver interface {
|
||||||
Organization(ctx context.Context, obj *types.Viewer) (*types.Organization, error)
|
Organization(ctx context.Context, obj *types.User) (*types.Organization, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type executableSchema struct {
|
type executableSchema struct {
|
||||||
@@ -1058,6 +1066,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.Query.Viewer(childComplexity), true
|
return e.complexity.Query.Viewer(childComplexity), true
|
||||||
|
|
||||||
|
case "Session.expiresAt":
|
||||||
|
if e.complexity.Session.ExpiresAt == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Session.ExpiresAt(childComplexity), true
|
||||||
|
|
||||||
|
case "Session.id":
|
||||||
|
if e.complexity.Session.ID == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Session.ID(childComplexity), true
|
||||||
|
|
||||||
case "Task.createdAt":
|
case "Task.createdAt":
|
||||||
if e.complexity.Task.CreatedAt == nil {
|
if e.complexity.Task.CreatedAt == nil {
|
||||||
break
|
break
|
||||||
@@ -1222,6 +1244,41 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.TaskStateTransitionEdge.Node(childComplexity), true
|
return e.complexity.TaskStateTransitionEdge.Node(childComplexity), true
|
||||||
|
|
||||||
|
case "User.createdAt":
|
||||||
|
if e.complexity.User.CreatedAt == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.User.CreatedAt(childComplexity), true
|
||||||
|
|
||||||
|
case "User.email":
|
||||||
|
if e.complexity.User.Email == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.User.Email(childComplexity), true
|
||||||
|
|
||||||
|
case "User.id":
|
||||||
|
if e.complexity.User.ID == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.User.ID(childComplexity), true
|
||||||
|
|
||||||
|
case "User.organization":
|
||||||
|
if e.complexity.User.Organization == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.User.Organization(childComplexity), true
|
||||||
|
|
||||||
|
case "User.updatedAt":
|
||||||
|
if e.complexity.User.UpdatedAt == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.User.UpdatedAt(childComplexity), true
|
||||||
|
|
||||||
case "Vendor.createdAt":
|
case "Vendor.createdAt":
|
||||||
if e.complexity.Vendor.CreatedAt == nil {
|
if e.complexity.Vendor.CreatedAt == nil {
|
||||||
break
|
break
|
||||||
@@ -1341,20 +1398,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.VendorEdge.Node(childComplexity), true
|
return e.complexity.VendorEdge.Node(childComplexity), true
|
||||||
|
|
||||||
case "Viewer.id":
|
|
||||||
if e.complexity.Viewer.ID == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.complexity.Viewer.ID(childComplexity), true
|
|
||||||
|
|
||||||
case "Viewer.organization":
|
|
||||||
if e.complexity.Viewer.Organization == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.complexity.Viewer.Organization(childComplexity), true
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
@@ -1810,14 +1853,25 @@ type EvidenceStateTransition {
|
|||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
# Authentication types
|
||||||
node(id: ID!): Node!
|
type User {
|
||||||
viewer: Viewer!
|
id: ID!
|
||||||
|
email: String!
|
||||||
|
|
||||||
|
organization: Organization! @goField(forceResolver: true)
|
||||||
|
|
||||||
|
createdAt: Datetime!
|
||||||
|
updatedAt: Datetime!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Viewer {
|
type Session {
|
||||||
id: ID!
|
id: ID!
|
||||||
organization: Organization! @goField(forceResolver: true)
|
expiresAt: Datetime!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
node(id: ID!): Node!
|
||||||
|
viewer: User!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
@@ -6671,9 +6725,9 @@ func (ec *executionContext) _Query_viewer(ctx context.Context, field graphql.Col
|
|||||||
}
|
}
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(*types.Viewer)
|
res := resTmp.(*types.User)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalNViewer2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐViewer(ctx, field.Selections, res)
|
return ec.marshalNUser2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUser(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) fieldContext_Query_viewer(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
func (ec *executionContext) fieldContext_Query_viewer(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
@@ -6685,11 +6739,17 @@ func (ec *executionContext) fieldContext_Query_viewer(_ context.Context, field g
|
|||||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
switch field.Name {
|
switch field.Name {
|
||||||
case "id":
|
case "id":
|
||||||
return ec.fieldContext_Viewer_id(ctx, field)
|
return ec.fieldContext_User_id(ctx, field)
|
||||||
|
case "email":
|
||||||
|
return ec.fieldContext_User_email(ctx, field)
|
||||||
case "organization":
|
case "organization":
|
||||||
return ec.fieldContext_Viewer_organization(ctx, field)
|
return ec.fieldContext_User_organization(ctx, field)
|
||||||
|
case "createdAt":
|
||||||
|
return ec.fieldContext_User_createdAt(ctx, field)
|
||||||
|
case "updatedAt":
|
||||||
|
return ec.fieldContext_User_updatedAt(ctx, field)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("no field named %q was found under type Viewer", field.Name)
|
return nil, fmt.Errorf("no field named %q was found under type User", field.Name)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return fc, nil
|
return fc, nil
|
||||||
@@ -6806,6 +6866,82 @@ func (ec *executionContext) fieldContext_Query___schema(_ context.Context, field
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Session_id(ctx context.Context, field graphql.CollectedField, obj *types.Session) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_Session_id(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.ID, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(gid.GID)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_Session_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "Session",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type ID does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Session_expiresAt(ctx context.Context, field graphql.CollectedField, obj *types.Session) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_Session_expiresAt(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.ExpiresAt, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(time.Time)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_Session_expiresAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "Session",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Datetime does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Task_id(ctx context.Context, field graphql.CollectedField, obj *types.Task) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Task_id(ctx context.Context, field graphql.CollectedField, obj *types.Task) (ret graphql.Marshaler) {
|
||||||
fc, err := ec.fieldContext_Task_id(ctx, field)
|
fc, err := ec.fieldContext_Task_id(ctx, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -7722,6 +7858,214 @@ func (ec *executionContext) fieldContext_TaskStateTransitionEdge_node(_ context.
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _User_id(ctx context.Context, field graphql.CollectedField, obj *types.User) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_User_id(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.ID, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(gid.GID)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_User_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "User",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type ID does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _User_email(ctx context.Context, field graphql.CollectedField, obj *types.User) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_User_email(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Email, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(string)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNString2string(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_User_email(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "User",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type String does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _User_organization(ctx context.Context, field graphql.CollectedField, obj *types.User) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_User_organization(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return ec.resolvers.User().Organization(rctx, obj)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*types.Organization)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNOrganization2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganization(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_User_organization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "User",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: true,
|
||||||
|
IsResolver: true,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
switch field.Name {
|
||||||
|
case "id":
|
||||||
|
return ec.fieldContext_Organization_id(ctx, field)
|
||||||
|
case "name":
|
||||||
|
return ec.fieldContext_Organization_name(ctx, field)
|
||||||
|
case "logoUrl":
|
||||||
|
return ec.fieldContext_Organization_logoUrl(ctx, field)
|
||||||
|
case "frameworks":
|
||||||
|
return ec.fieldContext_Organization_frameworks(ctx, field)
|
||||||
|
case "vendors":
|
||||||
|
return ec.fieldContext_Organization_vendors(ctx, field)
|
||||||
|
case "peoples":
|
||||||
|
return ec.fieldContext_Organization_peoples(ctx, field)
|
||||||
|
case "createdAt":
|
||||||
|
return ec.fieldContext_Organization_createdAt(ctx, field)
|
||||||
|
case "updatedAt":
|
||||||
|
return ec.fieldContext_Organization_updatedAt(ctx, field)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _User_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.User) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_User_createdAt(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.CreatedAt, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(time.Time)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_User_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "User",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Datetime does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _User_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.User) (ret graphql.Marshaler) {
|
||||||
|
fc, err := ec.fieldContext_User_updatedAt(ctx, field)
|
||||||
|
if err != nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.UpdatedAt, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(time.Time)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_User_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "User",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Datetime does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Vendor_id(ctx context.Context, field graphql.CollectedField, obj *types.Vendor) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Vendor_id(ctx context.Context, field graphql.CollectedField, obj *types.Vendor) (ret graphql.Marshaler) {
|
||||||
fc, err := ec.fieldContext_Vendor_id(ctx, field)
|
fc, err := ec.fieldContext_Vendor_id(ctx, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -8400,100 +8744,6 @@ func (ec *executionContext) fieldContext_VendorEdge_node(_ context.Context, fiel
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Viewer_id(ctx context.Context, field graphql.CollectedField, obj *types.Viewer) (ret graphql.Marshaler) {
|
|
||||||
fc, err := ec.fieldContext_Viewer_id(ctx, field)
|
|
||||||
if err != nil {
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
ctx = graphql.WithFieldContext(ctx, fc)
|
|
||||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
|
||||||
ctx = rctx // use context from middleware stack in children
|
|
||||||
return obj.ID, nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
ec.Error(ctx, err)
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
if resTmp == nil {
|
|
||||||
if !graphql.HasFieldError(ctx, fc) {
|
|
||||||
ec.Errorf(ctx, "must not be null")
|
|
||||||
}
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
res := resTmp.(gid.GID)
|
|
||||||
fc.Result = res
|
|
||||||
return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) fieldContext_Viewer_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
|
||||||
fc = &graphql.FieldContext{
|
|
||||||
Object: "Viewer",
|
|
||||||
Field: field,
|
|
||||||
IsMethod: false,
|
|
||||||
IsResolver: false,
|
|
||||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
|
||||||
return nil, errors.New("field of type ID does not have child fields")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return fc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) _Viewer_organization(ctx context.Context, field graphql.CollectedField, obj *types.Viewer) (ret graphql.Marshaler) {
|
|
||||||
fc, err := ec.fieldContext_Viewer_organization(ctx, field)
|
|
||||||
if err != nil {
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
ctx = graphql.WithFieldContext(ctx, fc)
|
|
||||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
|
|
||||||
ctx = rctx // use context from middleware stack in children
|
|
||||||
return ec.resolvers.Viewer().Organization(rctx, obj)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
ec.Error(ctx, err)
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
if resTmp == nil {
|
|
||||||
if !graphql.HasFieldError(ctx, fc) {
|
|
||||||
ec.Errorf(ctx, "must not be null")
|
|
||||||
}
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
res := resTmp.(*types.Organization)
|
|
||||||
fc.Result = res
|
|
||||||
return ec.marshalNOrganization2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganization(ctx, field.Selections, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) fieldContext_Viewer_organization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
|
||||||
fc = &graphql.FieldContext{
|
|
||||||
Object: "Viewer",
|
|
||||||
Field: field,
|
|
||||||
IsMethod: true,
|
|
||||||
IsResolver: true,
|
|
||||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
|
||||||
switch field.Name {
|
|
||||||
case "id":
|
|
||||||
return ec.fieldContext_Organization_id(ctx, field)
|
|
||||||
case "name":
|
|
||||||
return ec.fieldContext_Organization_name(ctx, field)
|
|
||||||
case "logoUrl":
|
|
||||||
return ec.fieldContext_Organization_logoUrl(ctx, field)
|
|
||||||
case "frameworks":
|
|
||||||
return ec.fieldContext_Organization_frameworks(ctx, field)
|
|
||||||
case "vendors":
|
|
||||||
return ec.fieldContext_Organization_vendors(ctx, field)
|
|
||||||
case "peoples":
|
|
||||||
return ec.fieldContext_Organization_peoples(ctx, field)
|
|
||||||
case "createdAt":
|
|
||||||
return ec.fieldContext_Organization_createdAt(ctx, field)
|
|
||||||
case "updatedAt":
|
|
||||||
return ec.fieldContext_Organization_updatedAt(ctx, field)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return fc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
|
func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
|
||||||
fc, err := ec.fieldContext___Directive_name(ctx, field)
|
fc, err := ec.fieldContext___Directive_name(ctx, field)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -12032,6 +12282,50 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sessionImplementors = []string{"Session"}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Session(ctx context.Context, sel ast.SelectionSet, obj *types.Session) graphql.Marshaler {
|
||||||
|
fields := graphql.CollectFields(ec.OperationContext, sel, sessionImplementors)
|
||||||
|
|
||||||
|
out := graphql.NewFieldSet(fields)
|
||||||
|
deferred := make(map[string]*graphql.FieldSet)
|
||||||
|
for i, field := range fields {
|
||||||
|
switch field.Name {
|
||||||
|
case "__typename":
|
||||||
|
out.Values[i] = graphql.MarshalString("Session")
|
||||||
|
case "id":
|
||||||
|
out.Values[i] = ec._Session_id(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
out.Invalids++
|
||||||
|
}
|
||||||
|
case "expiresAt":
|
||||||
|
out.Values[i] = ec._Session_expiresAt(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
out.Invalids++
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("unknown field " + strconv.Quote(field.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.Dispatch(ctx)
|
||||||
|
if out.Invalids > 0 {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
|
||||||
|
|
||||||
|
for label, dfs := range deferred {
|
||||||
|
ec.processDeferredGroup(graphql.DeferredGroup{
|
||||||
|
Label: label,
|
||||||
|
Path: graphql.GetPath(ctx),
|
||||||
|
FieldSet: dfs,
|
||||||
|
Context: ctx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
var taskImplementors = []string{"Task", "Node"}
|
var taskImplementors = []string{"Task", "Node"}
|
||||||
|
|
||||||
func (ec *executionContext) _Task(ctx context.Context, sel ast.SelectionSet, obj *types.Task) graphql.Marshaler {
|
func (ec *executionContext) _Task(ctx context.Context, sel ast.SelectionSet, obj *types.Task) graphql.Marshaler {
|
||||||
@@ -12392,6 +12686,91 @@ func (ec *executionContext) _TaskStateTransitionEdge(ctx context.Context, sel as
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userImplementors = []string{"User"}
|
||||||
|
|
||||||
|
func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *types.User) graphql.Marshaler {
|
||||||
|
fields := graphql.CollectFields(ec.OperationContext, sel, userImplementors)
|
||||||
|
|
||||||
|
out := graphql.NewFieldSet(fields)
|
||||||
|
deferred := make(map[string]*graphql.FieldSet)
|
||||||
|
for i, field := range fields {
|
||||||
|
switch field.Name {
|
||||||
|
case "__typename":
|
||||||
|
out.Values[i] = graphql.MarshalString("User")
|
||||||
|
case "id":
|
||||||
|
out.Values[i] = ec._User_id(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
atomic.AddUint32(&out.Invalids, 1)
|
||||||
|
}
|
||||||
|
case "email":
|
||||||
|
out.Values[i] = ec._User_email(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
atomic.AddUint32(&out.Invalids, 1)
|
||||||
|
}
|
||||||
|
case "organization":
|
||||||
|
field := field
|
||||||
|
|
||||||
|
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
|
||||||
|
res = ec._User_organization(ctx, field, obj)
|
||||||
|
if res == graphql.Null {
|
||||||
|
atomic.AddUint32(&fs.Invalids, 1)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Deferrable != nil {
|
||||||
|
dfs, ok := deferred[field.Deferrable.Label]
|
||||||
|
di := 0
|
||||||
|
if ok {
|
||||||
|
dfs.AddField(field)
|
||||||
|
di = len(dfs.Values) - 1
|
||||||
|
} else {
|
||||||
|
dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
|
||||||
|
deferred[field.Deferrable.Label] = dfs
|
||||||
|
}
|
||||||
|
dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
|
||||||
|
return innerFunc(ctx, dfs)
|
||||||
|
})
|
||||||
|
|
||||||
|
// don't run the out.Concurrently() call below
|
||||||
|
out.Values[i] = graphql.Null
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
|
||||||
|
case "createdAt":
|
||||||
|
out.Values[i] = ec._User_createdAt(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
atomic.AddUint32(&out.Invalids, 1)
|
||||||
|
}
|
||||||
|
case "updatedAt":
|
||||||
|
out.Values[i] = ec._User_updatedAt(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
atomic.AddUint32(&out.Invalids, 1)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("unknown field " + strconv.Quote(field.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.Dispatch(ctx)
|
||||||
|
if out.Invalids > 0 {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
|
||||||
|
|
||||||
|
for label, dfs := range deferred {
|
||||||
|
ec.processDeferredGroup(graphql.DeferredGroup{
|
||||||
|
Label: label,
|
||||||
|
Path: graphql.GetPath(ctx),
|
||||||
|
FieldSet: dfs,
|
||||||
|
Context: ctx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
var vendorImplementors = []string{"Vendor", "Node"}
|
var vendorImplementors = []string{"Vendor", "Node"}
|
||||||
|
|
||||||
func (ec *executionContext) _Vendor(ctx context.Context, sel ast.SelectionSet, obj *types.Vendor) graphql.Marshaler {
|
func (ec *executionContext) _Vendor(ctx context.Context, sel ast.SelectionSet, obj *types.Vendor) graphql.Marshaler {
|
||||||
@@ -12567,76 +12946,6 @@ func (ec *executionContext) _VendorEdge(ctx context.Context, sel ast.SelectionSe
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
var viewerImplementors = []string{"Viewer"}
|
|
||||||
|
|
||||||
func (ec *executionContext) _Viewer(ctx context.Context, sel ast.SelectionSet, obj *types.Viewer) graphql.Marshaler {
|
|
||||||
fields := graphql.CollectFields(ec.OperationContext, sel, viewerImplementors)
|
|
||||||
|
|
||||||
out := graphql.NewFieldSet(fields)
|
|
||||||
deferred := make(map[string]*graphql.FieldSet)
|
|
||||||
for i, field := range fields {
|
|
||||||
switch field.Name {
|
|
||||||
case "__typename":
|
|
||||||
out.Values[i] = graphql.MarshalString("Viewer")
|
|
||||||
case "id":
|
|
||||||
out.Values[i] = ec._Viewer_id(ctx, field, obj)
|
|
||||||
if out.Values[i] == graphql.Null {
|
|
||||||
atomic.AddUint32(&out.Invalids, 1)
|
|
||||||
}
|
|
||||||
case "organization":
|
|
||||||
field := field
|
|
||||||
|
|
||||||
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
|
|
||||||
res = ec._Viewer_organization(ctx, field, obj)
|
|
||||||
if res == graphql.Null {
|
|
||||||
atomic.AddUint32(&fs.Invalids, 1)
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
if field.Deferrable != nil {
|
|
||||||
dfs, ok := deferred[field.Deferrable.Label]
|
|
||||||
di := 0
|
|
||||||
if ok {
|
|
||||||
dfs.AddField(field)
|
|
||||||
di = len(dfs.Values) - 1
|
|
||||||
} else {
|
|
||||||
dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
|
|
||||||
deferred[field.Deferrable.Label] = dfs
|
|
||||||
}
|
|
||||||
dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
|
|
||||||
return innerFunc(ctx, dfs)
|
|
||||||
})
|
|
||||||
|
|
||||||
// don't run the out.Concurrently() call below
|
|
||||||
out.Values[i] = graphql.Null
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
|
|
||||||
default:
|
|
||||||
panic("unknown field " + strconv.Quote(field.Name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out.Dispatch(ctx)
|
|
||||||
if out.Invalids > 0 {
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
|
|
||||||
|
|
||||||
for label, dfs := range deferred {
|
|
||||||
ec.processDeferredGroup(graphql.DeferredGroup{
|
|
||||||
Label: label,
|
|
||||||
Path: graphql.GetPath(ctx),
|
|
||||||
FieldSet: dfs,
|
|
||||||
Context: ctx,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
var __DirectiveImplementors = []string{"__Directive"}
|
var __DirectiveImplementors = []string{"__Directive"}
|
||||||
|
|
||||||
func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler {
|
func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler {
|
||||||
@@ -13957,6 +14266,20 @@ func (ec *executionContext) unmarshalNUpdateVendorInput2githubᚗcomᚋgetprobo
|
|||||||
return res, graphql.ErrorOnPath(ctx, err)
|
return res, graphql.ErrorOnPath(ctx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNUser2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUser(ctx context.Context, sel ast.SelectionSet, v types.User) graphql.Marshaler {
|
||||||
|
return ec._User(ctx, sel, &v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNUser2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐUser(ctx context.Context, sel ast.SelectionSet, v *types.User) graphql.Marshaler {
|
||||||
|
if v == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
||||||
|
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
return ec._User(ctx, sel, v)
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) marshalNVendor2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐVendor(ctx context.Context, sel ast.SelectionSet, v types.Vendor) graphql.Marshaler {
|
func (ec *executionContext) marshalNVendor2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐVendor(ctx context.Context, sel ast.SelectionSet, v types.Vendor) graphql.Marshaler {
|
||||||
return ec._Vendor(ctx, sel, &v)
|
return ec._Vendor(ctx, sel, &v)
|
||||||
}
|
}
|
||||||
@@ -14033,20 +14356,6 @@ func (ec *executionContext) marshalNVendorEdge2ᚖgithubᚗcomᚋgetproboᚋprob
|
|||||||
return ec._VendorEdge(ctx, sel, v)
|
return ec._VendorEdge(ctx, sel, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) marshalNViewer2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐViewer(ctx context.Context, sel ast.SelectionSet, v types.Viewer) graphql.Marshaler {
|
|
||||||
return ec._Viewer(ctx, sel, &v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) marshalNViewer2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐViewer(ctx context.Context, sel ast.SelectionSet, v *types.Viewer) graphql.Marshaler {
|
|
||||||
if v == nil {
|
|
||||||
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
|
||||||
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
|
|
||||||
}
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
return ec._Viewer(ctx, sel, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler {
|
func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler {
|
||||||
return ec.___Directive(ctx, sel, &v)
|
return ec.___Directive(ctx, sel, &v)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,6 +220,11 @@ type PeopleEdge struct {
|
|||||||
type Query struct {
|
type Query struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
ID gid.GID `json:"id"`
|
||||||
|
ExpiresAt time.Time `json:"expiresAt"`
|
||||||
|
}
|
||||||
|
|
||||||
type Task struct {
|
type Task struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -286,6 +291,14 @@ type UpdateVendorInput struct {
|
|||||||
PrivacyPolicyURL *string `json:"privacyPolicyUrl,omitempty"`
|
PrivacyPolicyURL *string `json:"privacyPolicyUrl,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID gid.GID `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Organization *Organization `json:"organization"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
type Vendor struct {
|
type Vendor struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -314,8 +327,3 @@ type VendorEdge struct {
|
|||||||
Cursor page.CursorKey `json:"cursor"`
|
Cursor page.CursorKey `json:"cursor"`
|
||||||
Node *Vendor `json:"node"`
|
Node *Vendor `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Viewer struct {
|
|
||||||
ID gid.GID `json:"id"`
|
|
||||||
Organization *Organization `json:"organization"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
func (r *controlResolver) StateTransisions(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.ControlStateTransitionConnection, error) {
|
func (r *controlResolver) StateTransisions(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.ControlStateTransitionConnection, error) {
|
||||||
cursor := types.NewCursor(first, after, last, before)
|
cursor := types.NewCursor(first, after, last, before)
|
||||||
|
|
||||||
page, err := r.svc.ListControlStateTransitions(ctx, obj.ID, cursor)
|
page, err := r.proboSvc.ListControlStateTransitions(ctx, obj.ID, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot list control tasks: %w", err)
|
return nil, fmt.Errorf("cannot list control tasks: %w", err)
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ func (r *controlResolver) StateTransisions(ctx context.Context, obj *types.Contr
|
|||||||
func (r *controlResolver) Tasks(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskConnection, error) {
|
func (r *controlResolver) Tasks(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskConnection, error) {
|
||||||
cursor := types.NewCursor(first, after, last, before)
|
cursor := types.NewCursor(first, after, last, before)
|
||||||
|
|
||||||
page, err := r.svc.ListControlTasks(ctx, obj.ID, cursor)
|
page, err := r.proboSvc.ListControlTasks(ctx, obj.ID, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot list control tasks: %w", err)
|
return nil, fmt.Errorf("cannot list control tasks: %w", err)
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ func (r *controlResolver) Tasks(ctx context.Context, obj *types.Control, first *
|
|||||||
func (r *evidenceResolver) StateTransisions(ctx context.Context, obj *types.Evidence, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceStateTransitionConnection, error) {
|
func (r *evidenceResolver) StateTransisions(ctx context.Context, obj *types.Evidence, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceStateTransitionConnection, error) {
|
||||||
cursor := types.NewCursor(first, after, last, before)
|
cursor := types.NewCursor(first, after, last, before)
|
||||||
|
|
||||||
page, err := r.svc.ListEvidenceStateTransitions(ctx, obj.ID, cursor)
|
page, err := r.proboSvc.ListEvidenceStateTransitions(ctx, obj.ID, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot list evidence state transitions: %w", err)
|
return nil, fmt.Errorf("cannot list evidence state transitions: %w", err)
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ func (r *evidenceResolver) StateTransisions(ctx context.Context, obj *types.Evid
|
|||||||
func (r *frameworkResolver) Controls(ctx context.Context, obj *types.Framework, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.ControlConnection, error) {
|
func (r *frameworkResolver) Controls(ctx context.Context, obj *types.Framework, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.ControlConnection, error) {
|
||||||
cursor := types.NewCursor(first, after, last, before)
|
cursor := types.NewCursor(first, after, last, before)
|
||||||
|
|
||||||
page, err := r.svc.ListFrameworkControls(ctx, obj.ID, cursor)
|
page, err := r.proboSvc.ListFrameworkControls(ctx, obj.ID, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot list framework controls: %w", err)
|
return nil, fmt.Errorf("cannot list framework controls: %w", err)
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ func (r *frameworkResolver) Controls(ctx context.Context, obj *types.Framework,
|
|||||||
|
|
||||||
// CreateVendor is the resolver for the createVendor field.
|
// CreateVendor is the resolver for the createVendor field.
|
||||||
func (r *mutationResolver) CreateVendor(ctx context.Context, input types.CreateVendorInput) (*types.CreateVendorPayload, error) {
|
func (r *mutationResolver) CreateVendor(ctx context.Context, input types.CreateVendorInput) (*types.CreateVendorPayload, error) {
|
||||||
vendor, err := r.svc.CreateVendor(ctx, probo.CreateVendorRequest{
|
vendor, err := r.proboSvc.CreateVendor(ctx, probo.CreateVendorRequest{
|
||||||
OrganizationID: input.OrganizationID,
|
OrganizationID: input.OrganizationID,
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
Description: input.Description,
|
Description: input.Description,
|
||||||
@@ -89,7 +89,7 @@ func (r *mutationResolver) CreateVendor(ctx context.Context, input types.CreateV
|
|||||||
|
|
||||||
// UpdateVendor is the resolver for the updateVendor field.
|
// UpdateVendor is the resolver for the updateVendor field.
|
||||||
func (r *mutationResolver) UpdateVendor(ctx context.Context, input types.UpdateVendorInput) (*types.Vendor, error) {
|
func (r *mutationResolver) UpdateVendor(ctx context.Context, input types.UpdateVendorInput) (*types.Vendor, error) {
|
||||||
vendor, err := r.svc.UpdateVendor(ctx, probo.UpdateVendorRequest{
|
vendor, err := r.proboSvc.UpdateVendor(ctx, probo.UpdateVendorRequest{
|
||||||
ID: input.ID,
|
ID: input.ID,
|
||||||
ExpectedVersion: input.ExpectedVersion,
|
ExpectedVersion: input.ExpectedVersion,
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
@@ -111,7 +111,7 @@ func (r *mutationResolver) UpdateVendor(ctx context.Context, input types.UpdateV
|
|||||||
|
|
||||||
// DeleteVendor is the resolver for the deleteVendor field.
|
// DeleteVendor is the resolver for the deleteVendor field.
|
||||||
func (r *mutationResolver) DeleteVendor(ctx context.Context, input types.DeleteVendorInput) (*types.DeleteVendorPayload, error) {
|
func (r *mutationResolver) DeleteVendor(ctx context.Context, input types.DeleteVendorInput) (*types.DeleteVendorPayload, error) {
|
||||||
err := r.svc.DeleteVendor(ctx, input.VendorID)
|
err := r.proboSvc.DeleteVendor(ctx, input.VendorID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot delete vendor: %w", err)
|
return nil, fmt.Errorf("cannot delete vendor: %w", err)
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,7 @@ func (r *mutationResolver) DeleteVendor(ctx context.Context, input types.DeleteV
|
|||||||
|
|
||||||
// CreatePeople is the resolver for the createPeople field.
|
// CreatePeople is the resolver for the createPeople field.
|
||||||
func (r *mutationResolver) CreatePeople(ctx context.Context, input types.CreatePeopleInput) (*types.CreatePeoplePayload, error) {
|
func (r *mutationResolver) CreatePeople(ctx context.Context, input types.CreatePeopleInput) (*types.CreatePeoplePayload, error) {
|
||||||
people, err := r.svc.CreatePeople(ctx, probo.CreatePeopleRequest{
|
people, err := r.proboSvc.CreatePeople(ctx, probo.CreatePeopleRequest{
|
||||||
OrganizationID: input.OrganizationID,
|
OrganizationID: input.OrganizationID,
|
||||||
FullName: input.FullName,
|
FullName: input.FullName,
|
||||||
PrimaryEmailAddress: input.PrimaryEmailAddress,
|
PrimaryEmailAddress: input.PrimaryEmailAddress,
|
||||||
@@ -142,7 +142,7 @@ func (r *mutationResolver) CreatePeople(ctx context.Context, input types.CreateP
|
|||||||
|
|
||||||
// UpdatePeople is the resolver for the updatePeople field.
|
// UpdatePeople is the resolver for the updatePeople field.
|
||||||
func (r *mutationResolver) UpdatePeople(ctx context.Context, input types.UpdatePeopleInput) (*types.People, error) {
|
func (r *mutationResolver) UpdatePeople(ctx context.Context, input types.UpdatePeopleInput) (*types.People, error) {
|
||||||
people, err := r.svc.UpdatePeople(ctx, probo.UpdatePeopleRequest{
|
people, err := r.proboSvc.UpdatePeople(ctx, probo.UpdatePeopleRequest{
|
||||||
ID: input.ID,
|
ID: input.ID,
|
||||||
ExpectedVersion: input.ExpectedVersion,
|
ExpectedVersion: input.ExpectedVersion,
|
||||||
FullName: input.FullName,
|
FullName: input.FullName,
|
||||||
@@ -159,7 +159,7 @@ func (r *mutationResolver) UpdatePeople(ctx context.Context, input types.UpdateP
|
|||||||
|
|
||||||
// DeletePeople is the resolver for the deletePeople field.
|
// DeletePeople is the resolver for the deletePeople field.
|
||||||
func (r *mutationResolver) DeletePeople(ctx context.Context, input types.DeletePeopleInput) (*types.DeletePeoplePayload, error) {
|
func (r *mutationResolver) DeletePeople(ctx context.Context, input types.DeletePeopleInput) (*types.DeletePeoplePayload, error) {
|
||||||
err := r.svc.DeletePeople(ctx, input.PeopleID)
|
err := r.proboSvc.DeletePeople(ctx, input.PeopleID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot delete people: %w", err)
|
return nil, fmt.Errorf("cannot delete people: %w", err)
|
||||||
}
|
}
|
||||||
@@ -173,7 +173,7 @@ func (r *mutationResolver) DeletePeople(ctx context.Context, input types.DeleteP
|
|||||||
func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) {
|
func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.FrameworkConnection, error) {
|
||||||
cursor := types.NewCursor(first, after, last, before)
|
cursor := types.NewCursor(first, after, last, before)
|
||||||
|
|
||||||
page, err := r.svc.ListOrganizationFrameworks(ctx, obj.ID, cursor)
|
page, err := r.proboSvc.ListOrganizationFrameworks(ctx, obj.ID, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot list organization frameworks: %w", err)
|
return nil, fmt.Errorf("cannot list organization frameworks: %w", err)
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@ func (r *organizationResolver) Frameworks(ctx context.Context, obj *types.Organi
|
|||||||
func (r *organizationResolver) Vendors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.VendorConnection, error) {
|
func (r *organizationResolver) Vendors(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.VendorConnection, error) {
|
||||||
cursor := types.NewCursor(first, after, last, before)
|
cursor := types.NewCursor(first, after, last, before)
|
||||||
|
|
||||||
page, err := r.svc.ListOrganizationVendors(ctx, obj.ID, cursor)
|
page, err := r.proboSvc.ListOrganizationVendors(ctx, obj.ID, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot list organization vendors: %w", err)
|
return nil, fmt.Errorf("cannot list organization vendors: %w", err)
|
||||||
}
|
}
|
||||||
@@ -197,7 +197,7 @@ func (r *organizationResolver) Vendors(ctx context.Context, obj *types.Organizat
|
|||||||
func (r *organizationResolver) Peoples(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.PeopleConnection, error) {
|
func (r *organizationResolver) Peoples(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.PeopleConnection, error) {
|
||||||
cursor := types.NewCursor(first, after, last, before)
|
cursor := types.NewCursor(first, after, last, before)
|
||||||
|
|
||||||
page, err := r.svc.ListOrganizationPeoples(ctx, obj.ID, cursor)
|
page, err := r.proboSvc.ListOrganizationPeoples(ctx, obj.ID, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot list organization peoples: %w", err)
|
return nil, fmt.Errorf("cannot list organization peoples: %w", err)
|
||||||
}
|
}
|
||||||
@@ -209,49 +209,49 @@ func (r *organizationResolver) Peoples(ctx context.Context, obj *types.Organizat
|
|||||||
func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error) {
|
func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error) {
|
||||||
switch id.EntityType() {
|
switch id.EntityType() {
|
||||||
case coredata.OrganizationEntityType:
|
case coredata.OrganizationEntityType:
|
||||||
organization, err := r.svc.GetOrganization(ctx, id)
|
organization, err := r.proboSvc.GetOrganization(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewOrganization(organization), nil
|
return types.NewOrganization(organization), nil
|
||||||
case coredata.PeopleEntityType:
|
case coredata.PeopleEntityType:
|
||||||
people, err := r.svc.GetPeople(ctx, id)
|
people, err := r.proboSvc.GetPeople(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewPeople(people), nil
|
return types.NewPeople(people), nil
|
||||||
case coredata.VendorEntityType:
|
case coredata.VendorEntityType:
|
||||||
vendor, err := r.svc.GetVendor(ctx, id)
|
vendor, err := r.proboSvc.GetVendor(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewVendor(vendor), nil
|
return types.NewVendor(vendor), nil
|
||||||
case coredata.FrameworkEntityType:
|
case coredata.FrameworkEntityType:
|
||||||
framework, err := r.svc.GetFramework(ctx, id)
|
framework, err := r.proboSvc.GetFramework(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewFramework(framework), nil
|
return types.NewFramework(framework), nil
|
||||||
case coredata.ControlEntityType:
|
case coredata.ControlEntityType:
|
||||||
control, err := r.svc.GetControl(ctx, id)
|
control, err := r.proboSvc.GetControl(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewControl(control), nil
|
return types.NewControl(control), nil
|
||||||
case coredata.TaskEntityType:
|
case coredata.TaskEntityType:
|
||||||
task, err := r.svc.GetTask(ctx, id)
|
task, err := r.proboSvc.GetTask(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewTask(task), nil
|
return types.NewTask(task), nil
|
||||||
case coredata.EvidenceEntityType:
|
case coredata.EvidenceEntityType:
|
||||||
evidence, err := r.svc.GetEvidence(ctx, id)
|
evidence, err := r.proboSvc.GetEvidence(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -264,15 +264,21 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Viewer is the resolver for the viewer field.
|
// Viewer is the resolver for the viewer field.
|
||||||
func (r *queryResolver) Viewer(ctx context.Context) (*types.Viewer, error) {
|
func (r *queryResolver) Viewer(ctx context.Context) (*types.User, error) {
|
||||||
return &types.Viewer{}, nil
|
user := UserFromContext(ctx)
|
||||||
|
return &types.User{
|
||||||
|
ID: user.ID,
|
||||||
|
Email: user.EmailAddress,
|
||||||
|
CreatedAt: user.CreatedAt,
|
||||||
|
UpdatedAt: user.UpdatedAt,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StateTransisions is the resolver for the stateTransisions field.
|
// StateTransisions is the resolver for the stateTransisions field.
|
||||||
func (r *taskResolver) StateTransisions(ctx context.Context, obj *types.Task, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskStateTransitionConnection, error) {
|
func (r *taskResolver) StateTransisions(ctx context.Context, obj *types.Task, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.TaskStateTransitionConnection, error) {
|
||||||
cursor := types.NewCursor(first, after, last, before)
|
cursor := types.NewCursor(first, after, last, before)
|
||||||
|
|
||||||
page, err := r.svc.ListTaskStateTransitions(ctx, obj.ID, cursor)
|
page, err := r.proboSvc.ListTaskStateTransitions(ctx, obj.ID, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot list control tasks: %w", err)
|
return nil, fmt.Errorf("cannot list control tasks: %w", err)
|
||||||
}
|
}
|
||||||
@@ -284,7 +290,7 @@ func (r *taskResolver) StateTransisions(ctx context.Context, obj *types.Task, fi
|
|||||||
func (r *taskResolver) Evidences(ctx context.Context, obj *types.Task, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceConnection, error) {
|
func (r *taskResolver) Evidences(ctx context.Context, obj *types.Task, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceConnection, error) {
|
||||||
cursor := types.NewCursor(first, after, last, before)
|
cursor := types.NewCursor(first, after, last, before)
|
||||||
|
|
||||||
page, err := r.svc.ListTaskEvidences(ctx, obj.ID, cursor)
|
page, err := r.proboSvc.ListTaskEvidences(ctx, obj.ID, cursor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot list organization frameworks: %w", err)
|
return nil, fmt.Errorf("cannot list organization frameworks: %w", err)
|
||||||
}
|
}
|
||||||
@@ -293,11 +299,22 @@ func (r *taskResolver) Evidences(ctx context.Context, obj *types.Task, first *in
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Organization is the resolver for the organization field.
|
// Organization is the resolver for the organization field.
|
||||||
func (r *viewerResolver) Organization(ctx context.Context, obj *types.Viewer) (*types.Organization, error) {
|
func (r *userResolver) Organization(ctx context.Context, obj *types.User) (*types.Organization, error) {
|
||||||
organizationID, _ := gid.ParseGID("AZSfP_xAcAC5IAAAAAAltA") // TODO: remove this
|
// Get the user's organization ID
|
||||||
organization, err := r.svc.GetOrganization(ctx, organizationID)
|
organizationID, err := r.usrmgrSvc.GetUserOrganization(ctx, obj.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot get organization: %w", err)
|
return nil, fmt.Errorf("failed to get user organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user doesn't have an organization, return nil
|
||||||
|
if organizationID == gid.Nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the organization details
|
||||||
|
organization, err := r.proboSvc.GetOrganization(ctx, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get organization details: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.NewOrganization(organization), nil
|
return types.NewOrganization(organization), nil
|
||||||
@@ -324,8 +341,8 @@ func (r *Resolver) Query() schema.QueryResolver { return &queryResolver{r} }
|
|||||||
// Task returns schema.TaskResolver implementation.
|
// Task returns schema.TaskResolver implementation.
|
||||||
func (r *Resolver) Task() schema.TaskResolver { return &taskResolver{r} }
|
func (r *Resolver) Task() schema.TaskResolver { return &taskResolver{r} }
|
||||||
|
|
||||||
// Viewer returns schema.ViewerResolver implementation.
|
// User returns schema.UserResolver implementation.
|
||||||
func (r *Resolver) Viewer() schema.ViewerResolver { return &viewerResolver{r} }
|
func (r *Resolver) User() schema.UserResolver { return &userResolver{r} }
|
||||||
|
|
||||||
type controlResolver struct{ *Resolver }
|
type controlResolver struct{ *Resolver }
|
||||||
type evidenceResolver struct{ *Resolver }
|
type evidenceResolver struct{ *Resolver }
|
||||||
@@ -334,4 +351,4 @@ type mutationResolver struct{ *Resolver }
|
|||||||
type organizationResolver struct{ *Resolver }
|
type organizationResolver struct{ *Resolver }
|
||||||
type queryResolver struct{ *Resolver }
|
type queryResolver struct{ *Resolver }
|
||||||
type taskResolver struct{ *Resolver }
|
type taskResolver struct{ *Resolver }
|
||||||
type viewerResolver struct{ *Resolver }
|
type userResolver struct{ *Resolver }
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ var (
|
|||||||
Nil = GID(uuid.Nil)
|
Nil = GID(uuid.Nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ParseGID parses a string representation of a GID
|
||||||
func ParseGID(encoded string) (GID, error) {
|
func ParseGID(encoded string) (GID, error) {
|
||||||
gid := GID{}
|
gid := GID{}
|
||||||
|
|
||||||
@@ -42,6 +43,17 @@ func ParseGID(encoded string) (GID, error) {
|
|||||||
return gid, nil
|
return gid, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new GID with a default entity type of 0
|
||||||
|
func New() GID {
|
||||||
|
id, err := NewGID(0)
|
||||||
|
if err != nil {
|
||||||
|
// This should never happen with a valid random source
|
||||||
|
panic(fmt.Sprintf("failed to generate GID: %v", err))
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGID creates a new GID with the specified entity type
|
||||||
func NewGID(et uint32) (GID, error) {
|
func NewGID(et uint32) (GID, error) {
|
||||||
id, err := uuid.NewV7()
|
id, err := uuid.NewV7()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
56
pkg/probod/auth_config.go
Normal file
56
pkg/probod/auth_config.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package probod
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
authConfig struct {
|
||||||
|
// Pepper is a secret key used for password hashing
|
||||||
|
// It should be at least 32 bytes long
|
||||||
|
Pepper string `json:"pepper"`
|
||||||
|
SessionDuration int `json:"session-duration"`
|
||||||
|
CookieName string `json:"cookie-name"`
|
||||||
|
CookieSecure bool `json:"cookie-secure"`
|
||||||
|
CookieHTTPOnly bool `json:"cookie-http-only"`
|
||||||
|
CookieDomain string `json:"cookie-domain"`
|
||||||
|
CookiePath string `json:"cookie-path"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetPepperBytes returns the pepper as a byte array
|
||||||
|
func (c authConfig) GetPepperBytes() ([]byte, error) {
|
||||||
|
if c.Pepper == "" {
|
||||||
|
return nil, fmt.Errorf("pepper cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the pepper is base64 encoded, decode it
|
||||||
|
if decoded, err := base64.StdEncoding.DecodeString(c.Pepper); err == nil {
|
||||||
|
if len(decoded) < 32 {
|
||||||
|
return nil, fmt.Errorf("decoded pepper must be at least 32 bytes long")
|
||||||
|
}
|
||||||
|
return decoded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise use the raw string as the pepper
|
||||||
|
if len(c.Pepper) < 32 {
|
||||||
|
return nil, fmt.Errorf("pepper must be at least 32 bytes long")
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(c.Pepper), nil
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/getprobo/probo/pkg/api"
|
"github.com/getprobo/probo/pkg/api"
|
||||||
|
console_v1 "github.com/getprobo/probo/pkg/api/console/v1"
|
||||||
"github.com/getprobo/probo/pkg/probo"
|
"github.com/getprobo/probo/pkg/probo"
|
||||||
"github.com/getprobo/probo/pkg/usrmgr"
|
"github.com/getprobo/probo/pkg/usrmgr"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
@@ -40,8 +41,9 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
config struct {
|
config struct {
|
||||||
Pg pgConfig `json:"pg"`
|
Pg pgConfig `json:"pg"`
|
||||||
Api apiConfig `json:"api"`
|
Api apiConfig `json:"api"`
|
||||||
|
Auth authConfig `json:"auth"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,6 +68,15 @@ func New() *Implm {
|
|||||||
Database: "probod",
|
Database: "probod",
|
||||||
PoolSize: 100,
|
PoolSize: 100,
|
||||||
},
|
},
|
||||||
|
Auth: authConfig{
|
||||||
|
Pepper: "this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes",
|
||||||
|
SessionDuration: 24,
|
||||||
|
CookieName: "SSID",
|
||||||
|
CookieSecure: false,
|
||||||
|
CookieHTTPOnly: true,
|
||||||
|
CookieDomain: "localhost",
|
||||||
|
CookiePath: "/",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,21 +106,36 @@ func (impl *Implm) Run(
|
|||||||
return fmt.Errorf("cannot create pg client: %w", err)
|
return fmt.Errorf("cannot create pg client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
usrmgr, err := usrmgr.NewService(ctx, pgClient)
|
// Get the pepper bytes for password hashing
|
||||||
|
pepper, err := impl.cfg.Auth.GetPepperBytes()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot get pepper bytes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the user management service with the pepper
|
||||||
|
usrmgrService, err := usrmgr.NewService(ctx, pgClient, pepper)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot create usrmgr service: %w", err)
|
return fmt.Errorf("cannot create usrmgr service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
probo, err := probo.NewService(ctx, pgClient)
|
proboService, err := probo.NewService(ctx, pgClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot create probo service: %w", err)
|
return fmt.Errorf("cannot create probo service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
apiServer, err := api.NewServer(
|
apiServer, err := api.NewServer(
|
||||||
api.Config{
|
api.Config{
|
||||||
Probo: probo,
|
Probo: proboService,
|
||||||
Usrmgr: usrmgr,
|
Usrmgr: usrmgrService,
|
||||||
AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins,
|
AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins,
|
||||||
|
Auth: console_v1.AuthConfig{
|
||||||
|
CookieName: impl.cfg.Auth.CookieName,
|
||||||
|
CookieSecure: impl.cfg.Auth.CookieSecure,
|
||||||
|
CookieHTTPOnly: impl.cfg.Auth.CookieHTTPOnly,
|
||||||
|
CookieDomain: impl.cfg.Auth.CookieDomain,
|
||||||
|
CookiePath: impl.cfg.Auth.CookiePath,
|
||||||
|
SessionDuration: time.Duration(impl.cfg.Auth.SessionDuration) * time.Hour,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
5
pkg/usrmgr/coredata/migrations/20250225T153220Z.sql
Normal file
5
pkg/usrmgr/coredata/migrations/20250225T153220Z.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- Add organization_id column to usrmgr_users table
|
||||||
|
ALTER TABLE usrmgr_users ADD COLUMN organization_id TEXT REFERENCES organizations(id);
|
||||||
|
|
||||||
|
-- Create an index for faster lookups
|
||||||
|
CREATE INDEX usrmgr_users_organization_id_idx ON usrmgr_users(organization_id);
|
||||||
@@ -16,7 +16,6 @@ package coredata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/getprobo/probo/pkg/gid"
|
"github.com/getprobo/probo/pkg/gid"
|
||||||
@@ -57,17 +56,17 @@ func (s *Session) LoadByID(
|
|||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
|
user_id,
|
||||||
|
expired_at,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
sessions
|
usrmgr_sessions
|
||||||
WHERE
|
WHERE
|
||||||
id = @session_id
|
id = @session_id
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
`
|
`
|
||||||
|
|
||||||
q = fmt.Sprintf(q)
|
|
||||||
|
|
||||||
args := pgx.NamedArgs{"session_id": sessionID}
|
args := pgx.NamedArgs{"session_id": sessionID}
|
||||||
|
|
||||||
r := conn.QueryRow(ctx, q, args)
|
r := conn.QueryRow(ctx, q, args)
|
||||||
@@ -88,7 +87,7 @@ func (s *Session) Insert(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
sessions (id, user_id, expired_at, created_at, updated_at)
|
usrmgr_sessions (id, user_id, expired_at, created_at, updated_at)
|
||||||
VALUES (
|
VALUES (
|
||||||
@session_id,
|
@session_id,
|
||||||
@user_id,
|
@user_id,
|
||||||
@@ -109,3 +108,44 @@ VALUES (
|
|||||||
_, err := conn.Exec(ctx, q, args)
|
_, err := conn.Exec(ctx, q, args)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Session) Update(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
UPDATE usrmgr_sessions
|
||||||
|
SET
|
||||||
|
expired_at = @expired_at,
|
||||||
|
updated_at = @updated_at
|
||||||
|
WHERE
|
||||||
|
id = @session_id
|
||||||
|
`
|
||||||
|
|
||||||
|
args := pgx.NamedArgs{
|
||||||
|
"session_id": s.ID,
|
||||||
|
"expired_at": s.ExpiredAt,
|
||||||
|
"updated_at": s.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteSession(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
sessionID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
DELETE FROM
|
||||||
|
usrmgr_sessions
|
||||||
|
WHERE
|
||||||
|
id = @session_id
|
||||||
|
`
|
||||||
|
|
||||||
|
args := pgx.NamedArgs{"session_id": sessionID}
|
||||||
|
|
||||||
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type (
|
|||||||
ID gid.GID
|
ID gid.GID
|
||||||
EmailAddress string
|
EmailAddress string
|
||||||
HashedPassword []byte
|
HashedPassword []byte
|
||||||
|
OrganizationID gid.GID
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
@@ -43,6 +44,7 @@ func (u *User) scan(r pgx.Row) error {
|
|||||||
&u.ID,
|
&u.ID,
|
||||||
&u.EmailAddress,
|
&u.EmailAddress,
|
||||||
&u.HashedPassword,
|
&u.HashedPassword,
|
||||||
|
&u.OrganizationID,
|
||||||
&u.CreatedAt,
|
&u.CreatedAt,
|
||||||
&u.UpdatedAt,
|
&u.UpdatedAt,
|
||||||
)
|
)
|
||||||
@@ -58,12 +60,13 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
email_address,
|
email_address,
|
||||||
hashed_password,
|
hashed_password,
|
||||||
|
organization_id,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
users
|
usrmgr_users
|
||||||
WHERE
|
WHERE
|
||||||
email = @user_email
|
email_address = @user_email
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -80,3 +83,67 @@ LIMIT 1;
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) LoadByID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
userID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
email_address,
|
||||||
|
hashed_password,
|
||||||
|
organization_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
usrmgr_users
|
||||||
|
WHERE
|
||||||
|
id = @user_id
|
||||||
|
LIMIT 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
args := pgx.NamedArgs{"user_id": userID}
|
||||||
|
|
||||||
|
r := conn.QueryRow(ctx, q, args)
|
||||||
|
|
||||||
|
u2 := User{}
|
||||||
|
if err := u2.scan(r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*u = u2
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Insert(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
INSERT INTO
|
||||||
|
usrmgr_users (id, email_address, hashed_password, organization_id, created_at, updated_at)
|
||||||
|
VALUES (
|
||||||
|
@user_id,
|
||||||
|
@email_address,
|
||||||
|
@hashed_password,
|
||||||
|
@organization_id,
|
||||||
|
@created_at,
|
||||||
|
@updated_at
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
args := pgx.NamedArgs{
|
||||||
|
"user_id": u.ID,
|
||||||
|
"email_address": u.EmailAddress,
|
||||||
|
"hashed_password": u.HashedPassword,
|
||||||
|
"organization_id": "AZSfP_xAcAC5IAAAAAAltA",
|
||||||
|
"created_at": u.CreatedAt,
|
||||||
|
"updated_at": u.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
|
|
||||||
"github.com/getprobo/probo/pkg/gid"
|
"github.com/getprobo/probo/pkg/gid"
|
||||||
"github.com/getprobo/probo/pkg/usrmgr/coredata"
|
"github.com/getprobo/probo/pkg/usrmgr/coredata"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
"go.gearno.de/kit/migrator"
|
"go.gearno.de/kit/migrator"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
)
|
)
|
||||||
@@ -30,22 +31,116 @@ type (
|
|||||||
pg *pg.Client
|
pg *pg.Client
|
||||||
hp *HashingProfile
|
hp *HashingProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RegisterUserParams struct {
|
||||||
|
Email string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrInvalidCredentials struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrUserAlreadyExists struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrSessionNotFound struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrSessionExpired struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (e ErrInvalidCredentials) Error() string {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrUserAlreadyExists) Error() string {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrSessionNotFound) Error() string {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrSessionExpired) Error() string {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
pgClient *pg.Client,
|
pgClient *pg.Client,
|
||||||
|
pepper []byte,
|
||||||
) (*Service, error) {
|
) (*Service, error) {
|
||||||
err := migrator.NewMigrator(pgClient, coredata.Migrations).Run(ctx, "migrations")
|
err := migrator.NewMigrator(pgClient, coredata.Migrations).Run(ctx, "migrations")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot migrate database schema: %w", err)
|
return nil, fmt.Errorf("cannot migrate database schema: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hp, err := NewHashingProfile(pepper)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot create hashing profile: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
pg: pgClient,
|
pg: pgClient,
|
||||||
|
hp: hp,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Service) RegisterUser(
|
||||||
|
ctx context.Context,
|
||||||
|
params RegisterUserParams,
|
||||||
|
) (*coredata.User, error) {
|
||||||
|
if params.Email == "" || params.Password == "" {
|
||||||
|
return nil, fmt.Errorf("email and password are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a high iteration count for password hashing
|
||||||
|
const iterations = 600000
|
||||||
|
hashedPassword, err := s.hp.HashPassword([]byte(params.Password), iterations)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot hash password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
user := &coredata.User{
|
||||||
|
ID: gid.New(),
|
||||||
|
EmailAddress: params.Email,
|
||||||
|
HashedPassword: hashedPassword,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
// Check if user already exists
|
||||||
|
existingUser := &coredata.User{}
|
||||||
|
err := existingUser.LoadByEmail(ctx, tx, params.Email)
|
||||||
|
if err == nil {
|
||||||
|
return &ErrUserAlreadyExists{message: "user with this email already exists"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the new user
|
||||||
|
if err := user.Insert(ctx, tx); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s Service) Login(
|
func (s Service) Login(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
email string,
|
email string,
|
||||||
@@ -54,8 +149,8 @@ func (s Service) Login(
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
user := &coredata.User{}
|
user := &coredata.User{}
|
||||||
session := &coredata.Session{
|
session := &coredata.Session{
|
||||||
ID: gid.GID{},
|
ID: gid.New(),
|
||||||
UserID: user.ID,
|
UserID: gid.GID{}, // Will be set after user is loaded
|
||||||
ExpiredAt: now.Add(24 * time.Hour),
|
ExpiredAt: now.Add(24 * time.Hour),
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
@@ -65,18 +160,21 @@ func (s Service) Login(
|
|||||||
ctx,
|
ctx,
|
||||||
func(tx pg.Conn) error {
|
func(tx pg.Conn) error {
|
||||||
if err := user.LoadByEmail(ctx, tx, email); err != nil {
|
if err := user.LoadByEmail(ctx, tx, email); err != nil {
|
||||||
return fmt.Errorf("cannot load user by email: %w", err)
|
return &ErrInvalidCredentials{message: "invalid email or password"}
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, err := s.hp.ComparePasswordAndHash([]byte(password), user.HashedPassword)
|
ok, err := s.hp.ComparePasswordAndHash([]byte(password), user.HashedPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot constant compare byte: %w", err)
|
return fmt.Errorf("cannot compare password: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("invalid password")
|
return &ErrInvalidCredentials{message: "invalid email or password"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the user ID in the session
|
||||||
|
session.UserID = user.ID
|
||||||
|
|
||||||
if err := session.Insert(ctx, tx); err != nil {
|
if err := session.Insert(ctx, tx); err != nil {
|
||||||
return fmt.Errorf("cannot insert session: %w", err)
|
return fmt.Errorf("cannot insert session: %w", err)
|
||||||
}
|
}
|
||||||
@@ -89,13 +187,176 @@ func (s Service) Login(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) Logout(sessionID string) error {
|
func (s Service) Logout(
|
||||||
return nil
|
ctx context.Context,
|
||||||
|
sessionID gid.GID,
|
||||||
|
) error {
|
||||||
|
return s.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
return coredata.DeleteSession(ctx, tx, sessionID)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) GetSession(sessionID string) (*coredata.Session, error) {
|
func (s Service) GetSession(
|
||||||
return nil, nil
|
ctx context.Context,
|
||||||
|
sessionID gid.GID,
|
||||||
|
) (*coredata.Session, error) {
|
||||||
|
session := &coredata.Session{}
|
||||||
|
|
||||||
|
err := s.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
if err := session.LoadByID(ctx, tx, sessionID); err != nil {
|
||||||
|
return &ErrSessionNotFound{message: "session not found"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if session is expired
|
||||||
|
if time.Now().After(session.ExpiredAt) {
|
||||||
|
// Delete expired session
|
||||||
|
if err := coredata.DeleteSession(ctx, tx, sessionID); err != nil {
|
||||||
|
return fmt.Errorf("cannot delete expired session: %w", err)
|
||||||
|
}
|
||||||
|
return &ErrSessionExpired{message: "session expired"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Service) RefreshSession(
|
||||||
|
ctx context.Context,
|
||||||
|
sessionID gid.GID,
|
||||||
|
) (*coredata.Session, error) {
|
||||||
|
session := &coredata.Session{}
|
||||||
|
|
||||||
|
err := s.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
if err := session.LoadByID(ctx, tx, sessionID); err != nil {
|
||||||
|
return &ErrSessionNotFound{message: "session not found"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if session is expired
|
||||||
|
if time.Now().After(session.ExpiredAt) {
|
||||||
|
return &ErrSessionExpired{message: "session expired"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update session expiration
|
||||||
|
now := time.Now()
|
||||||
|
session.ExpiredAt = now.Add(24 * time.Hour)
|
||||||
|
session.UpdatedAt = now
|
||||||
|
|
||||||
|
if err := session.Update(ctx, tx); err != nil {
|
||||||
|
return fmt.Errorf("cannot update session: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Service) GetUserByID(
|
||||||
|
ctx context.Context,
|
||||||
|
userID gid.GID,
|
||||||
|
) (*coredata.User, error) {
|
||||||
|
user := &coredata.User{}
|
||||||
|
|
||||||
|
err := s.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
if err := user.LoadByID(ctx, tx, userID); err != nil {
|
||||||
|
return fmt.Errorf("user not found: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Service) GetUserBySession(
|
||||||
|
ctx context.Context,
|
||||||
|
sessionID gid.GID,
|
||||||
|
) (*coredata.User, error) {
|
||||||
|
session, err := s.GetSession(ctx, sessionID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetUserByID(ctx, session.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserOrganization sets the organization for a user
|
||||||
|
func (s Service) SetUserOrganization(
|
||||||
|
ctx context.Context,
|
||||||
|
userID gid.GID,
|
||||||
|
organizationID gid.GID,
|
||||||
|
) error {
|
||||||
|
return s.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
user := &coredata.User{}
|
||||||
|
if err := user.LoadByID(ctx, tx, userID); err != nil {
|
||||||
|
return fmt.Errorf("user not found: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the organization ID
|
||||||
|
user.OrganizationID = organizationID
|
||||||
|
user.UpdatedAt = time.Now()
|
||||||
|
|
||||||
|
// Update the user in the database
|
||||||
|
q := `
|
||||||
|
UPDATE usrmgr_users
|
||||||
|
SET organization_id = @organization_id, updated_at = @updated_at
|
||||||
|
WHERE id = @user_id
|
||||||
|
`
|
||||||
|
args := pgx.NamedArgs{
|
||||||
|
"user_id": user.ID,
|
||||||
|
"organization_id": user.OrganizationID,
|
||||||
|
"updated_at": user.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := tx.Exec(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot update user organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserOrganization gets the organization ID for a user
|
||||||
|
func (s Service) GetUserOrganization(
|
||||||
|
ctx context.Context,
|
||||||
|
userID gid.GID,
|
||||||
|
) (gid.GID, error) {
|
||||||
|
user, err := s.GetUserByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return gid.GID{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.OrganizationID, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user