@@ -22,16 +22,18 @@ import { NavUser_viewer$key } from "./__generated__/NavUser_viewer.graphql";
|
|||||||
import { buildEndpoint } from "@/utils";
|
import { buildEndpoint } from "@/utils";
|
||||||
|
|
||||||
export const navUserFragment = graphql`
|
export const navUserFragment = graphql`
|
||||||
fragment NavUser_viewer on User {
|
fragment NavUser_viewer on Viewer {
|
||||||
|
user {
|
||||||
id
|
id
|
||||||
fullName
|
fullName
|
||||||
email
|
email
|
||||||
}
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function NavUser({ viewer }: { viewer: NavUser_viewer$key }) {
|
export function NavUser({ viewer }: { viewer: NavUser_viewer$key }) {
|
||||||
const { isMobile } = useSidebar();
|
const { isMobile } = useSidebar();
|
||||||
const currentUser = useFragment(navUserFragment, viewer);
|
const currentUser = useFragment(navUserFragment, viewer).user;
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
fetch(buildEndpoint("/api/console/v1/auth/logout"), {
|
fetch(buildEndpoint("/api/console/v1/auth/logout"), {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export const organizationSwitcherFragment = graphql`
|
export const organizationSwitcherFragment = graphql`
|
||||||
fragment OrganizationSwitcher_organizations on User {
|
fragment OrganizationSwitcher_organizations on Viewer {
|
||||||
organizations(first: 25)
|
organizations(first: 25)
|
||||||
@connection(key: "OrganizationSwitcher_organizations") {
|
@connection(key: "OrganizationSwitcher_organizations") {
|
||||||
__id
|
__id
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<d4b79299641fcf7b2547eec4064af57a>>
|
* @generated SignedSource<<72d01b98680bc82c2d5bdc8c2fa2d49d>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -47,7 +47,7 @@ return {
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "User",
|
"concreteType": "Viewer",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
@@ -79,7 +79,7 @@ return {
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "User",
|
"concreteType": "Viewer",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
@@ -193,6 +193,15 @@ return {
|
|||||||
"kind": "LinkedHandle",
|
"kind": "LinkedHandle",
|
||||||
"name": "organizations"
|
"name": "organizations"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "User",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "user",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v0/*: any*/),
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -210,15 +219,18 @@ return {
|
|||||||
],
|
],
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "d92e5e742128d82b461ab1b6d52cd2a6",
|
"cacheID": "51c72d5d6b9e1d4e5e7f45e7280a0534",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "AppSidebarQuery",
|
"name": "AppSidebarQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query AppSidebarQuery {\n viewer {\n id\n ...OrganizationSwitcher_organizations\n ...NavUser_viewer\n }\n}\n\nfragment NavUser_viewer on User {\n id\n fullName\n email\n}\n\nfragment OrganizationSwitcher_organizations on User {\n organizations(first: 25) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
|
"text": "query AppSidebarQuery {\n viewer {\n id\n ...OrganizationSwitcher_organizations\n ...NavUser_viewer\n }\n}\n\nfragment NavUser_viewer on Viewer {\n user {\n id\n fullName\n email\n }\n}\n\nfragment OrganizationSwitcher_organizations on Viewer {\n organizations(first: 25) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<0965f25c8dedb59a9316e4df4d773af6>>
|
* @generated SignedSource<<0d5ada0f912fcf3a8f2f3328b0943b28>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -11,9 +11,11 @@
|
|||||||
import { ReaderFragment } from 'relay-runtime';
|
import { ReaderFragment } from 'relay-runtime';
|
||||||
import { FragmentRefs } from "relay-runtime";
|
import { FragmentRefs } from "relay-runtime";
|
||||||
export type NavUser_viewer$data = {
|
export type NavUser_viewer$data = {
|
||||||
|
readonly user: {
|
||||||
readonly email: string;
|
readonly email: string;
|
||||||
readonly fullName: string;
|
readonly fullName: string;
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
|
};
|
||||||
readonly " $fragmentType": "NavUser_viewer";
|
readonly " $fragmentType": "NavUser_viewer";
|
||||||
};
|
};
|
||||||
export type NavUser_viewer$key = {
|
export type NavUser_viewer$key = {
|
||||||
@@ -26,6 +28,14 @@ const node: ReaderFragment = {
|
|||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "NavUser_viewer",
|
"name": "NavUser_viewer",
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "User",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "user",
|
||||||
|
"plural": false,
|
||||||
"selections": [
|
"selections": [
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
@@ -49,10 +59,13 @@ const node: ReaderFragment = {
|
|||||||
"storageKey": null
|
"storageKey": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "User",
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Viewer",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
};
|
};
|
||||||
|
|
||||||
(node as any).hash = "7ee85a272f543b514c5a6a3148fd1613";
|
(node as any).hash = "05e26e238976738b4cff26f37c5f29e7";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<075aa1caf8416e0cce7536d0da9b5ca9>>
|
* @generated SignedSource<<152991faff70c7ef6c4751fac48090be>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -151,10 +151,10 @@ const node: ReaderFragment = {
|
|||||||
"storageKey": null
|
"storageKey": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "User",
|
"type": "Viewer",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
};
|
};
|
||||||
|
|
||||||
(node as any).hash = "8150704a009e88c096c6f6aa13cada52";
|
(node as any).hash = "4bc97175bc64a6c7554a6ec865d12850";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { OrganizationSelectionPageQuery } from "./__generated__/OrganizationSele
|
|||||||
const organizationSelectionQuery = graphql`
|
const organizationSelectionQuery = graphql`
|
||||||
query OrganizationSelectionPageQuery {
|
query OrganizationSelectionPageQuery {
|
||||||
viewer {
|
viewer {
|
||||||
id
|
|
||||||
organizations(first: 25) {
|
organizations(first: 25) {
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
@@ -34,11 +33,11 @@ export default function OrganizationSelectionPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const data = useLazyLoadQuery<OrganizationSelectionPageQuery>(
|
const data = useLazyLoadQuery<OrganizationSelectionPageQuery>(
|
||||||
organizationSelectionQuery,
|
organizationSelectionQuery,
|
||||||
{},
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
const organizations = data.viewer.organizations.edges.map(
|
const organizations = data.viewer.organizations.edges.map(
|
||||||
(edge) => edge.node,
|
(edge) => edge.node
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -45,6 +45,16 @@ const settingsPageQuery = graphql`
|
|||||||
... on Organization {
|
... on Organization {
|
||||||
name
|
name
|
||||||
logoUrl
|
logoUrl
|
||||||
|
users(first: 100) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
fullName
|
||||||
|
email
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,13 +74,6 @@ const updateOrganizationMutation = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface Member {
|
|
||||||
id: string;
|
|
||||||
fullName: string;
|
|
||||||
primaryEmailAddress: string;
|
|
||||||
role: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SettingsPageContent({
|
function SettingsPageContent({
|
||||||
queryRef,
|
queryRef,
|
||||||
}: {
|
}: {
|
||||||
@@ -78,11 +81,14 @@ function SettingsPageContent({
|
|||||||
}) {
|
}) {
|
||||||
const data = usePreloadedQuery(settingsPageQuery, queryRef);
|
const data = usePreloadedQuery(settingsPageQuery, queryRef);
|
||||||
const organization = data.organization;
|
const organization = data.organization;
|
||||||
const members: Member[] = [];
|
const users = organization.users?.edges.map((edge) => edge.node) || [];
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [isEditNameOpen, setIsEditNameOpen] = useState(false);
|
const [isEditNameOpen, setIsEditNameOpen] = useState(false);
|
||||||
|
const [isInviteOpen, setIsInviteOpen] = useState(false);
|
||||||
|
const [inviteEmail, setInviteEmail] = useState("");
|
||||||
|
const [inviteRole, setInviteRole] = useState("Member");
|
||||||
const [organizationName, setOrganizationName] = useState(
|
const [organizationName, setOrganizationName] = useState(
|
||||||
organization.name || ""
|
organization.name || ""
|
||||||
);
|
);
|
||||||
@@ -162,6 +168,19 @@ function SettingsPageContent({
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInviteMember = () => {
|
||||||
|
// This is a placeholder for the actual invite functionality
|
||||||
|
// In a real implementation, we would use a GraphQL mutation to invite the user
|
||||||
|
// For now, we just show a toast message and close the dialog
|
||||||
|
toast({
|
||||||
|
title: "Invitation sent",
|
||||||
|
description: `An invitation has been sent to ${inviteEmail}`,
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
setIsInviteOpen(false);
|
||||||
|
setInviteEmail("");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="container mx-auto p-6 space-y-6">
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
@@ -245,38 +264,59 @@ function SettingsPageContent({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<div>
|
||||||
<CardTitle>Workspace members</CardTitle>
|
<CardTitle>Workspace members</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Manage who has privileged access to your workspace and their
|
Manage who has privileged access to your workspace and their
|
||||||
permissions.
|
permissions.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsInviteOpen(true)}
|
||||||
|
>
|
||||||
|
<Upload className="mr-2 h-4 w-4" />
|
||||||
|
Invite member
|
||||||
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{members.map((member) => (
|
{users.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center p-8 text-center">
|
||||||
|
<div className="rounded-full bg-muted p-3">
|
||||||
|
<Building2 className="h-6 w-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<h3 className="mt-4 text-lg font-medium">No members found</h3>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
You haven't added any members to your workspace yet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
users.map((user) => (
|
||||||
<div
|
<div
|
||||||
key={member.id}
|
key={user.id}
|
||||||
className="flex items-center justify-between rounded-lg border p-3 shadow-xs"
|
className="flex items-center justify-between rounded-lg border p-3 shadow-xs"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
{member.fullName.charAt(0)}
|
{user.fullName.charAt(0).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
{member.fullName}
|
{user.fullName}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{member.primaryEmailAddress}
|
{user.email}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{member.role}
|
Owner
|
||||||
</span>
|
</span>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -286,7 +326,6 @@ function SettingsPageContent({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem>Change role</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem className="text-red-600">
|
<DropdownMenuItem className="text-red-600">
|
||||||
Remove member
|
Remove member
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -294,7 +333,8 @@ function SettingsPageContent({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -327,6 +367,48 @@ function SettingsPageContent({
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={isInviteOpen} onOpenChange={setIsInviteOpen}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Invite Team Member</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Send an invitation to join your workspace.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">Email Address</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={inviteEmail}
|
||||||
|
onChange={(e) => setInviteEmail(e.target.value)}
|
||||||
|
placeholder="Enter email address"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="role">Role</Label>
|
||||||
|
<select
|
||||||
|
id="role"
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
value={inviteRole}
|
||||||
|
onChange={(e) => setInviteRole(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="Admin">Admin</option>
|
||||||
|
<option value="Member">Member</option>
|
||||||
|
<option value="Viewer">Viewer</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsInviteOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleInviteMember}>Send Invitation</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<91e1415ad0a7d8ee0ae2a8e212f42f19>>
|
* @generated SignedSource<<371bd1a7433b281f20dd8925915a7ff2>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -25,7 +25,7 @@ var v0 = [
|
|||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
"concreteType": "User",
|
"concreteType": "Viewer",
|
||||||
"kind": "LinkedField",
|
"kind": "LinkedField",
|
||||||
"name": "viewer",
|
"name": "viewer",
|
||||||
"plural": false,
|
"plural": false,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<d50c4986386f83d9755379cd44eb7c2c>>
|
* @generated SignedSource<<282a94b76fe5e17bd40b45ced6445ef3>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -12,7 +12,6 @@ import { ConcreteRequest } from 'relay-runtime';
|
|||||||
export type OrganizationSelectionPageQuery$variables = Record<PropertyKey, never>;
|
export type OrganizationSelectionPageQuery$variables = Record<PropertyKey, never>;
|
||||||
export type OrganizationSelectionPageQuery$data = {
|
export type OrganizationSelectionPageQuery$data = {
|
||||||
readonly viewer: {
|
readonly viewer: {
|
||||||
readonly id: string;
|
|
||||||
readonly organizations: {
|
readonly organizations: {
|
||||||
readonly edges: ReadonlyArray<{
|
readonly edges: ReadonlyArray<{
|
||||||
readonly node: {
|
readonly node: {
|
||||||
@@ -37,17 +36,7 @@ var v0 = {
|
|||||||
"name": "id",
|
"name": "id",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
v1 = [
|
v1 = {
|
||||||
{
|
|
||||||
"alias": null,
|
|
||||||
"args": null,
|
|
||||||
"concreteType": "User",
|
|
||||||
"kind": "LinkedField",
|
|
||||||
"name": "viewer",
|
|
||||||
"plural": false,
|
|
||||||
"selections": [
|
|
||||||
(v0/*: any*/),
|
|
||||||
{
|
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": [
|
"args": [
|
||||||
{
|
{
|
||||||
@@ -100,18 +89,27 @@ v1 = [
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"storageKey": "organizations(first:25)"
|
"storageKey": "organizations(first:25)"
|
||||||
}
|
};
|
||||||
],
|
|
||||||
"storageKey": null
|
|
||||||
}
|
|
||||||
];
|
|
||||||
return {
|
return {
|
||||||
"fragment": {
|
"fragment": {
|
||||||
"argumentDefinitions": [],
|
"argumentDefinitions": [],
|
||||||
"kind": "Fragment",
|
"kind": "Fragment",
|
||||||
"metadata": null,
|
"metadata": null,
|
||||||
"name": "OrganizationSelectionPageQuery",
|
"name": "OrganizationSelectionPageQuery",
|
||||||
"selections": (v1/*: any*/),
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Viewer",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "viewer",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v1/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
"type": "Query",
|
"type": "Query",
|
||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
},
|
},
|
||||||
@@ -120,19 +118,33 @@ return {
|
|||||||
"argumentDefinitions": [],
|
"argumentDefinitions": [],
|
||||||
"kind": "Operation",
|
"kind": "Operation",
|
||||||
"name": "OrganizationSelectionPageQuery",
|
"name": "OrganizationSelectionPageQuery",
|
||||||
"selections": (v1/*: any*/)
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "Viewer",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "viewer",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v1/*: any*/),
|
||||||
|
(v0/*: any*/)
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "634e8f413a8b1f2685cfc1f4fd3c13cc",
|
"cacheID": "2938cb693a84c22366862de424a84d4b",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "OrganizationSelectionPageQuery",
|
"name": "OrganizationSelectionPageQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query OrganizationSelectionPageQuery {\n viewer {\n id\n organizations(first: 25) {\n edges {\n node {\n id\n name\n logoUrl\n }\n }\n }\n }\n}\n"
|
"text": "query OrganizationSelectionPageQuery {\n viewer {\n organizations(first: 25) {\n edges {\n node {\n id\n name\n logoUrl\n }\n }\n }\n id\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "786037ac4c8d61f3b7cf6b4536aadbbb";
|
(node as any).hash = "c41774fd198ab5f0f4b866d805099baf";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<eb02c3812f7904bcf7d937d1c29d9905>>
|
* @generated SignedSource<<7fcee268b2974653bc3734b49b4082a4>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -17,6 +17,16 @@ export type SettingsPageQuery$data = {
|
|||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly logoUrl?: string | null | undefined;
|
readonly logoUrl?: string | null | undefined;
|
||||||
readonly name?: string;
|
readonly name?: string;
|
||||||
|
readonly users?: {
|
||||||
|
readonly edges: ReadonlyArray<{
|
||||||
|
readonly node: {
|
||||||
|
readonly createdAt: string;
|
||||||
|
readonly email: string;
|
||||||
|
readonly fullName: string;
|
||||||
|
readonly id: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export type SettingsPageQuery = {
|
export type SettingsPageQuery = {
|
||||||
@@ -62,6 +72,67 @@ v3 = {
|
|||||||
"kind": "ScalarField",
|
"kind": "ScalarField",
|
||||||
"name": "logoUrl",
|
"name": "logoUrl",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Literal",
|
||||||
|
"name": "first",
|
||||||
|
"value": 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "UserConnection",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "users",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "UserEdge",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "edges",
|
||||||
|
"plural": true,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"concreteType": "User",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "node",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
(v2/*: any*/),
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "fullName",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "email",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "createdAt",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": "users(first:100)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "Organization",
|
"type": "Organization",
|
||||||
@@ -120,16 +191,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "1ff42392cdd403c92101dc9c5fb7ceda",
|
"cacheID": "de417aa8583ad2d1cc7ef18223a4dc2d",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "SettingsPageQuery",
|
"name": "SettingsPageQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query SettingsPageQuery(\n $organizationID: ID!\n) {\n organization: node(id: $organizationID) {\n __typename\n id\n ... on Organization {\n name\n logoUrl\n }\n }\n}\n"
|
"text": "query SettingsPageQuery(\n $organizationID: ID!\n) {\n organization: node(id: $organizationID) {\n __typename\n id\n ... on Organization {\n name\n logoUrl\n users(first: 100) {\n edges {\n node {\n id\n fullName\n email\n createdAt\n }\n }\n }\n }\n }\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "60cdb8a35f7e5f58536101243b57e57c";
|
(node as any).hash = "1d9482689dd9f305ca7a45fdf4ab088c";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -39,6 +40,8 @@ type (
|
|||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Users []*User
|
||||||
|
|
||||||
ErrUserNotFound struct {
|
ErrUserNotFound struct {
|
||||||
Identifier string
|
Identifier string
|
||||||
}
|
}
|
||||||
@@ -60,6 +63,50 @@ func (u User) CursorKey() page.CursorKey {
|
|||||||
return page.NewCursorKey(u.ID, u.CreatedAt)
|
return page.NewCursorKey(u.ID, u.CreatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *Users) LoadByOrganizationID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
organizationID gid.GID,
|
||||||
|
cursor *page.Cursor,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
email_address,
|
||||||
|
hashed_password,
|
||||||
|
email_address_verified,
|
||||||
|
fullname,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
users
|
||||||
|
WHERE
|
||||||
|
id IN (
|
||||||
|
SELECT user_id FROM users_organizations WHERE organization_id = @organization_id
|
||||||
|
)
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, cursor.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||||
|
maps.Copy(args, cursor.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query users: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
users, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[User])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect users: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*u = users
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *User) LoadByEmail(
|
func (u *User) LoadByEmail(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
|||||||
@@ -109,6 +109,13 @@ type Organization implements Node {
|
|||||||
name: String!
|
name: String!
|
||||||
logoUrl: String @goField(forceResolver: true)
|
logoUrl: String @goField(forceResolver: true)
|
||||||
|
|
||||||
|
users(
|
||||||
|
first: Int
|
||||||
|
after: CursorKey
|
||||||
|
last: Int
|
||||||
|
before: CursorKey
|
||||||
|
): UserConnection! @goField(forceResolver: true)
|
||||||
|
|
||||||
frameworks(
|
frameworks(
|
||||||
first: Int
|
first: Int
|
||||||
after: CursorKey
|
after: CursorKey
|
||||||
@@ -298,18 +305,20 @@ type Evidence implements Node {
|
|||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserConnection {
|
||||||
|
edges: [UserEdge!]!
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserEdge {
|
||||||
|
cursor: CursorKey!
|
||||||
|
node: User!
|
||||||
|
}
|
||||||
|
|
||||||
type User implements Node {
|
type User implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
fullName: String!
|
fullName: String!
|
||||||
email: String!
|
email: String!
|
||||||
|
|
||||||
organizations(
|
|
||||||
first: Int
|
|
||||||
after: CursorKey
|
|
||||||
last: Int
|
|
||||||
before: CursorKey
|
|
||||||
): OrganizationConnection! @goField(forceResolver: true)
|
|
||||||
|
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
}
|
}
|
||||||
@@ -321,7 +330,20 @@ type Session {
|
|||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
node(id: ID!): Node!
|
node(id: ID!): Node!
|
||||||
viewer: User!
|
viewer: Viewer!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Viewer {
|
||||||
|
id: ID!
|
||||||
|
|
||||||
|
user: User!
|
||||||
|
|
||||||
|
organizations(
|
||||||
|
first: Int
|
||||||
|
after: CursorKey
|
||||||
|
last: Int
|
||||||
|
before: CursorKey
|
||||||
|
): OrganizationConnection! @goField(forceResolver: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -254,6 +254,7 @@ type Organization struct {
|
|||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
LogoURL *string `json:"logoUrl,omitempty"`
|
LogoURL *string `json:"logoUrl,omitempty"`
|
||||||
|
Users *UserConnection `json:"users"`
|
||||||
Frameworks *FrameworkConnection `json:"frameworks"`
|
Frameworks *FrameworkConnection `json:"frameworks"`
|
||||||
Vendors *VendorConnection `json:"vendors"`
|
Vendors *VendorConnection `json:"vendors"`
|
||||||
Peoples *PeopleConnection `json:"peoples"`
|
Peoples *PeopleConnection `json:"peoples"`
|
||||||
@@ -480,7 +481,6 @@ type User struct {
|
|||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
FullName string `json:"fullName"`
|
FullName string `json:"fullName"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Organizations *OrganizationConnection `json:"organizations"`
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
@@ -488,6 +488,16 @@ type User struct {
|
|||||||
func (User) IsNode() {}
|
func (User) IsNode() {}
|
||||||
func (this User) GetID() gid.GID { return this.ID }
|
func (this User) GetID() gid.GID { return this.ID }
|
||||||
|
|
||||||
|
type UserConnection struct {
|
||||||
|
Edges []*UserEdge `json:"edges"`
|
||||||
|
PageInfo *PageInfo `json:"pageInfo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserEdge struct {
|
||||||
|
Cursor page.CursorKey `json:"cursor"`
|
||||||
|
Node *User `json:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
type Vendor struct {
|
type Vendor struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -516,3 +526,9 @@ 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"`
|
||||||
|
User *User `json:"user"`
|
||||||
|
Organizations *OrganizationConnection `json:"organizations"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,8 +16,28 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/getprobo/probo/pkg/coredata"
|
"github.com/getprobo/probo/pkg/coredata"
|
||||||
|
"github.com/getprobo/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func NewUserConnection(p *page.Page[*coredata.User]) *UserConnection {
|
||||||
|
var edges = make([]*UserEdge, len(p.Data))
|
||||||
|
|
||||||
|
for i := range edges {
|
||||||
|
edges[i] = NewUserEdge(p.Data[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UserConnection{
|
||||||
|
Edges: edges,
|
||||||
|
PageInfo: NewPageInfo(p),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserEdge(user *coredata.User) *UserEdge {
|
||||||
|
return &UserEdge{
|
||||||
|
Node: NewUser(user),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewUser(u *coredata.User) *User {
|
func NewUser(u *coredata.User) *User {
|
||||||
return &User{
|
return &User{
|
||||||
ID: u.ID,
|
ID: u.ID,
|
||||||
|
|||||||
@@ -517,6 +517,18 @@ func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organizat
|
|||||||
return svc.Organizations.GenerateLogoURL(ctx, obj.ID, 1*time.Hour)
|
return svc.Organizations.GenerateLogoURL(ctx, obj.ID, 1*time.Hour)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Users is the resolver for the users field.
|
||||||
|
func (r *organizationResolver) Users(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.UserConnection, error) {
|
||||||
|
cursor := types.NewCursor(first, after, last, before)
|
||||||
|
|
||||||
|
page, err := r.usrmgrSvc.ListUsersForTenant(ctx, obj.ID, cursor)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot list users: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewUserConnection(page), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Frameworks is the resolver for the frameworks field.
|
// Frameworks is the resolver for the frameworks field.
|
||||||
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) {
|
||||||
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
|
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
|
||||||
@@ -657,9 +669,14 @@ 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.User, error) {
|
func (r *queryResolver) Viewer(ctx context.Context) (*types.Viewer, error) {
|
||||||
user := UserFromContext(ctx)
|
user := UserFromContext(ctx)
|
||||||
return types.NewUser(user), nil
|
session := SessionFromContext(ctx)
|
||||||
|
|
||||||
|
return &types.Viewer{
|
||||||
|
ID: session.ID,
|
||||||
|
User: types.NewUser(user),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssignedTo is the resolver for the assignedTo field.
|
// AssignedTo is the resolver for the assignedTo field.
|
||||||
@@ -697,8 +714,10 @@ func (r *taskResolver) Evidences(ctx context.Context, obj *types.Task, first *in
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Organizations is the resolver for the organizations field.
|
// Organizations is the resolver for the organizations field.
|
||||||
func (r *userResolver) Organizations(ctx context.Context, obj *types.User, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.OrganizationConnection, error) {
|
func (r *viewerResolver) Organizations(ctx context.Context, obj *types.Viewer, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.OrganizationConnection, error) {
|
||||||
organizations, err := r.usrmgrSvc.ListOrganizationsForUserID(ctx, obj.ID)
|
user := UserFromContext(ctx)
|
||||||
|
|
||||||
|
organizations, err := r.usrmgrSvc.ListOrganizationsForUserID(ctx, user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("failed to list organizations for user: %w", err))
|
panic(fmt.Errorf("failed to list organizations for user: %w", err))
|
||||||
}
|
}
|
||||||
@@ -738,8 +757,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} }
|
||||||
|
|
||||||
// User returns schema.UserResolver implementation.
|
// Viewer returns schema.ViewerResolver implementation.
|
||||||
func (r *Resolver) User() schema.UserResolver { return &userResolver{r} }
|
func (r *Resolver) Viewer() schema.ViewerResolver { return &viewerResolver{r} }
|
||||||
|
|
||||||
type controlResolver struct{ *Resolver }
|
type controlResolver struct{ *Resolver }
|
||||||
type evidenceResolver struct{ *Resolver }
|
type evidenceResolver struct{ *Resolver }
|
||||||
@@ -749,4 +768,4 @@ type organizationResolver struct{ *Resolver }
|
|||||||
type policyResolver struct{ *Resolver }
|
type policyResolver struct{ *Resolver }
|
||||||
type queryResolver struct{ *Resolver }
|
type queryResolver struct{ *Resolver }
|
||||||
type taskResolver struct{ *Resolver }
|
type taskResolver struct{ *Resolver }
|
||||||
type userResolver struct{ *Resolver }
|
type viewerResolver struct{ *Resolver }
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/getprobo/probo/pkg/coredata"
|
"github.com/getprobo/probo/pkg/coredata"
|
||||||
"github.com/getprobo/probo/pkg/crypto/passwdhash"
|
"github.com/getprobo/probo/pkg/crypto/passwdhash"
|
||||||
"github.com/getprobo/probo/pkg/gid"
|
"github.com/getprobo/probo/pkg/gid"
|
||||||
|
"github.com/getprobo/probo/pkg/page"
|
||||||
"github.com/getprobo/probo/pkg/statelesstoken"
|
"github.com/getprobo/probo/pkg/statelesstoken"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
)
|
)
|
||||||
@@ -491,3 +492,24 @@ func (s Service) ConfirmEmail(ctx context.Context, tokenString string) error {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Service) ListUsersForTenant(
|
||||||
|
ctx context.Context,
|
||||||
|
organizationID gid.GID,
|
||||||
|
cursor *page.Cursor,
|
||||||
|
) (*page.Page[*coredata.User], error) {
|
||||||
|
users := coredata.Users{}
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(tx pg.Conn) error {
|
||||||
|
return users.LoadByOrganizationID(ctx, tx, organizationID, cursor)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return page.NewPage(users, cursor), nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user