manage deployment owners / collaborators

This commit is contained in:
mbecker20
2022-04-08 01:47:15 -07:00
parent cf8ce414ea
commit a0109c35de
22 changed files with 305 additions and 97 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ const ws = fp((app: FastifyInstance, _: {}, done: () => void) => {
app,
connection.socket,
JSON.parse(msg.toString()),
userID,
userID
)
);
connection.socket.on("close", unsub);
+92 -25
View File
@@ -2,6 +2,7 @@ import {
getContainerLog,
getContainerStatus,
intoCollection,
DEPLOYMENT_OWNER_UPDATE,
} from "@monitor/util";
import { FastifyInstance } from "fastify";
import fp from "fastify-plugin";
@@ -13,32 +14,36 @@ import {
} from "../util/periphery/container";
const deployments = fp((app: FastifyInstance, _: {}, done: () => void) => {
app.get("/api/deployments", { onRequest: [app.auth, app.userEnabled] }, async (req, res) => {
// returns the periphery deployments on the given serverID
// returns the core deployments if no serverID is specified
const { serverID } = req.query as { serverID?: string };
const server = serverID ? await app.servers.findById(serverID) : app.core;
if (!server) {
res.status(400);
res.send();
return;
app.get(
"/api/deployments",
{ onRequest: [app.auth, app.userEnabled] },
async (req, res) => {
// returns the periphery deployments on the given serverID
// returns the core deployments if no serverID is specified
const { serverID } = req.query as { serverID?: string };
const server = serverID ? await app.servers.findById(serverID) : app.core;
if (!server) {
res.status(400);
res.send();
return;
}
const deployments = await app.deployments.find(
{ serverID: server._id },
"name containerName serverID owners repo"
);
const status = server.isCore
? await deploymentStatusLocal(app)
: await getPeripheryContainers(server);
res.send(
intoCollection(
deployments.map((deployment) => ({
...deployment,
status: status[deployment.containerName!] || "not deployed",
}))
)
);
}
const deployments = await app.deployments.find(
{ serverID: server._id },
"name containerName serverID owners repo"
);
const status = server.isCore
? await deploymentStatusLocal(app)
: await getPeripheryContainers(server);
res.send(
intoCollection(
deployments.map((deployment) => ({
...deployment,
status: status[deployment.containerName!] || "not deployed",
}))
)
);
});
);
app.get(
"/api/deployment/:id",
@@ -142,6 +147,68 @@ const deployments = fp((app: FastifyInstance, _: {}, done: () => void) => {
}
);
app.post(
"/api/deployment/:id/:owner",
{ onRequest: [app.auth, app.userEnabled] },
async (req, res) => {
// adds an owner to a deployment
const { id, owner } = req.params as { id: string; owner: string };
const sender = (await app.users.findById(req.user.id))!;
if (sender.permissions! < 1) {
res.status(403);
res.send("inadequate permissions");
return;
}
const user = await app.users.findOne({ username: owner });
if (!user || user.permissions! < 1) {
res.status(400);
res.send("invalid user");
return;
}
const deployment = await app.deployments.findById(id);
if (!deployment) {
res.status(400);
res.send("deployment not found");
return;
}
if (
sender.permissions! < 2 &&
!deployment.owners.includes(sender.username)
) {
res.status(403);
res.send("inadequate permissions");
return;
}
await app.deployments.updateById(id, { $push: { owners: owner } });
app.broadcast(DEPLOYMENT_OWNER_UPDATE, { deploymentID: id });
res.send("owner added");
}
);
app.delete(
"/api/deployment/:id/:owner",
{ onRequest: [app.auth, app.userEnabled] },
async (req, res) => {
// removes owner from deployment
const { id, owner } = req.params as { id: string; owner: string };
const sender = (await app.users.findById(req.user.id))!;
if (sender.permissions! < 2) {
res.status(403);
res.send("inadequate permissions");
return;
}
const deployment = await app.deployments.findById(id);
if (!deployment) {
res.status(400);
res.send("deployment not found");
return;
}
await app.deployments.updateById(id, { $pull: { owners: owner } });
app.broadcast(DEPLOYMENT_OWNER_UPDATE, { deploymentID: id });
res.send("owner removed");
}
);
done();
});
+10 -2
View File
@@ -5,12 +5,20 @@ import fp from "fastify-plugin";
const users = fp((app: FastifyInstance, _: {}, done: () => void) => {
app.get("/api/users", { onRequest: [app.auth] }, async (req, res) => {
const user = await app.users.findById(req.user.id);
if (!user || !user.enabled || user.permissions! < 2) {
if (!user || !user.enabled || user.permissions! < 1) {
res.status(403);
res.send("not authorized");
}
const { username } = req.query as { username: string };
const users = await app.users.find(
{ permissions: { $lt: 2 } },
filterOutUndefined({
permissions: { $lt: 2 },
username: username
? {
$regex: `.*${username}.*`,
}
: undefined,
}),
"username permissions enabled"
);
res.send(users);
@@ -57,7 +57,7 @@ const DeploymentTabs: Component<{}> = () => {
onCleanup(unsub);
return (
<Show when={deployment()}>
<ConfigProvider deployment={deployment()!}>
<ConfigProvider>
<Tabs
containerClass="card tabs shadow"
containerStyle={{ gap: "0.5rem" }}
@@ -1,28 +1,30 @@
import { Component, Show } from "solid-js";
import Grid from "../../../util/layout/Grid";
import Image from "./Image";
import Network from "./Network";
import Mounts from "./Volumes";
import Env from "./Env";
import Ports from "./Ports";
import Image from "./container/Image";
import Network from "./container/Network";
import Mounts from "./container/Volumes";
import Env from "./container/Env";
import Ports from "./container/Ports";
import { useConfig } from "./Provider";
import Flex from "../../../util/layout/Flex";
import Icon from "../../../util/Icon";
import ConfirmButton from "../../../util/ConfirmButton";
import Restart from "./Restart";
import DockerAccount from "./DockerAccount";
import Git from "./Git";
import Restart from "./container/Restart";
import DockerAccount from "./container/DockerAccount";
import Git from "./mount-repo/Git";
import Tabs from "../../../util/tabs/Tabs";
import RepoMount from "./RepoMount";
import { OnClone, OnPull } from "./OnGit";
import RepoMount from "./mount-repo/RepoMount";
import { OnClone, OnPull } from "./mount-repo/OnGit";
import Loading from "../../../util/loading/Loading";
import Owners from "./Owners";
const Config: Component<{}> = (p) => {
const { deployment, reset, save } = useConfig();
const { deployment, reset, save, userCanUpdate } = useConfig();
return (
<Show when={deployment.loaded}>
<Grid class="config">
<Tabs
containerStyle={{ height: "100%" }}
tabsGap="0rem"
tabs={[
{
@@ -52,6 +54,14 @@ const Config: Component<{}> = (p) => {
</Grid>
),
},
userCanUpdate() && {
title: "collaborators",
element: (
<Grid class="config-items scroller">
<Owners />
</Grid>
),
},
]}
/>
<Show when={deployment.updated}>
@@ -0,0 +1,90 @@
import { User } from "@monitor/types";
import { Component, createEffect, createSignal, For, Show } from "solid-js";
import { pushNotification } from "../../../..";
import { useUser } from "../../../../state/UserProvider";
import {
addOwnerToDeployment,
getUsers,
removeOwnerFromDeployment,
} from "../../../../util/query";
import ConfirmButton from "../../../util/ConfirmButton";
import Input from "../../../util/Input";
import Flex from "../../../util/layout/Flex";
import Grid from "../../../util/layout/Grid";
import Menu from "../../../util/menu/Menu";
import { useConfig } from "./Provider";
const Owners: Component<{}> = (p) => {
const { deployment } = useConfig();
const { permissions } = useUser();
const [userSearch, setUserSearch] = createSignal("");
const [users, setUsers] = createSignal<User[]>([]);
createEffect(() => {
if (userSearch().length > 0) {
getUsers(userSearch()).then((users) => {
setUsers(
users.filter((user) => !deployment.owners.includes(user.username))
);
});
} else {
setUsers([]);
}
});
return (
<Grid class="config-item shadow">
<Menu
show={userSearch() ? true : false}
close={() => setUserSearch("")}
target={
<Input
placeholder="add user"
value={userSearch()}
onEdit={setUserSearch}
style={{ width: "12rem" }}
/>
}
content={
<>
<For each={users()}>
{(user) => (
<ConfirmButton
color="grey"
style={{ width: "100%", "justify-content": "flex-start" }}
onConfirm={async () => {
await addOwnerToDeployment(deployment._id!, user.username);
pushNotification("good", "owner added to deployment");
setUserSearch("");
}}
>
{user.username}
</ConfirmButton>
)}
</For>
<Show when={users().length === 0}>no matching users</Show>
</>
}
style={{ width: "12rem" }}
/>
<For each={deployment.owners}>
{(owner) => (
<Flex alignItems="center" justifyContent="space-between">
<div class="big-text">{owner}</div>
<Show when={permissions() > 1}>
<ConfirmButton
color="red"
onConfirm={async () => {
await removeOwnerFromDeployment(deployment._id!, owner);
pushNotification("good", "user added to collaborators");
}}
>
remove
</ConfirmButton>
</Show>
</Flex>
)}
</For>
</Grid>
);
};
export default Owners;
@@ -9,7 +9,11 @@ import {
useContext,
} from "solid-js";
import { createStore, DeepReadonly, SetStoreFunction } from "solid-js/store";
import { ADD_UPDATE, UPDATE_DEPLOYMENT } from "../../../../state/actions";
import {
ADD_UPDATE,
DEPLOYMENT_OWNER_UPDATE,
UPDATE_DEPLOYMENT,
} from "../../../../state/actions";
import { useAppState } from "../../../../state/StateProvider";
import { useUser } from "../../../../state/UserProvider";
import { getDeployment, getNetworks } from "../../../../util/query";
@@ -82,15 +86,27 @@ export const ConfigProvider: Component<{}> = (p) => {
ws.send(UPDATE_DEPLOYMENT, { deployment });
};
const unsub = ws.subscribe([ADD_UPDATE], ({ update }: { update: Update }) => {
if (update.deploymentID === selected.id()) {
if ([UPDATE_DEPLOYMENT].includes(update.operation)) {
load();
onCleanup(
ws.subscribe([ADD_UPDATE], ({ update }: { update: Update }) => {
if (update.deploymentID === selected.id()) {
if ([UPDATE_DEPLOYMENT].includes(update.operation)) {
load();
}
}
}
});
})
);
onCleanup(unsub);
onCleanup(
ws.subscribe(
[DEPLOYMENT_OWNER_UPDATE],
async ({ deploymentID }: { deploymentID: string }) => {
if (deploymentID === selected.id()) {
const dep = await getDeployment(selected.id());
set("owners", dep.owners);
}
}
)
);
const userCanUpdate = () => {
if (permissions() > 1) {
@@ -1,8 +1,8 @@
import { Component, Show } from "solid-js";
import { useAppState } from "../../../../state/StateProvider";
import Flex from "../../../util/layout/Flex";
import Selector from "../../../util/menu/Selector";
import { useConfig } from "./Provider";
import { useAppState } from "../../../../../state/StateProvider";
import Flex from "../../../../util/layout/Flex";
import Selector from "../../../../util/menu/Selector";
import { useConfig } from "../Provider";
const DockerAccount: Component<{}> = (p) => {
const { dockerAccounts } = useAppState();
@@ -1,9 +1,9 @@
import { Component, For, Show } from "solid-js";
import Icon from "../../../util/Icon";
import Input from "../../../util/Input";
import Flex from "../../../util/layout/Flex";
import Grid from "../../../util/layout/Grid";
import { useConfig } from "./Provider";
import Icon from "../../../../util/Icon";
import Input from "../../../../util/Input";
import Flex from "../../../../util/layout/Flex";
import Grid from "../../../../util/layout/Grid";
import { useConfig } from "../Provider";
const Env: Component<{}> = (p) => {
const { deployment, setDeployment, userCanUpdate } = useConfig();
@@ -1,11 +1,11 @@
import { Component, Show } from "solid-js";
import { useAppState } from "../../../../state/StateProvider";
import { useUser } from "../../../../state/UserProvider";
import { useAppState } from "../../../../../state/StateProvider";
import { useUser } from "../../../../../state/UserProvider";
// import { useToggle } from "../../../../util/hooks";
import Input from "../../../util/Input";
import Flex from "../../../util/layout/Flex";
import Selector from "../../../util/menu/Selector";
import { useConfig } from "./Provider";
import Input from "../../../../util/Input";
import Flex from "../../../../util/layout/Flex";
import Selector from "../../../../util/menu/Selector";
import { useConfig } from "../Provider";
const Image: Component<{}> = (p) => {
const { deployment, setDeployment, userCanUpdate } = useConfig();
@@ -1,7 +1,7 @@
import { Component } from "solid-js";
import Flex from "../../../util/layout/Flex";
import Selector from "../../../util/menu/Selector";
import { useConfig } from "./Provider";
import Flex from "../../../../util/layout/Flex";
import Selector from "../../../../util/menu/Selector";
import { useConfig } from "../Provider";
const Network: Component<{}> = (p) => {
const { deployment, setDeployment, networks, userCanUpdate } = useConfig();
@@ -1,9 +1,9 @@
import { Component, For, Show } from "solid-js";
import Icon from "../../../util/Icon";
import Input from "../../../util/Input";
import Flex from "../../../util/layout/Flex";
import Grid from "../../../util/layout/Grid";
import { useConfig } from "./Provider";
import Icon from "../../../../util/Icon";
import Input from "../../../../util/Input";
import Flex from "../../../../util/layout/Flex";
import Grid from "../../../../util/layout/Grid";
import { useConfig } from "../Provider";
const Ports: Component<{}> = (p) => {
const { deployment, setDeployment, userCanUpdate } = useConfig();
@@ -1,7 +1,7 @@
import { Component } from "solid-js";
import Flex from "../../../util/layout/Flex";
import Selector from "../../../util/menu/Selector";
import { useConfig } from "./Provider";
import Flex from "../../../../util/layout/Flex";
import Selector from "../../../../util/menu/Selector";
import { useConfig } from "../Provider";
const RESTART_MODES = [
"don't restart",
@@ -1,9 +1,9 @@
import { Component, For, Show } from "solid-js";
import Icon from "../../../util/Icon";
import Input from "../../../util/Input";
import Flex from "../../../util/layout/Flex";
import Grid from "../../../util/layout/Grid";
import { useConfig } from "./Provider";
import Icon from "../../../../util/Icon";
import Input from "../../../../util/Input";
import Flex from "../../../../util/layout/Flex";
import Grid from "../../../../util/layout/Grid";
import { useConfig } from "../Provider";
const Volumes: Component<{}> = (p) => {
const { deployment, setDeployment, userCanUpdate } = useConfig();
@@ -1,10 +1,10 @@
import { Component, Show } from "solid-js";
import { useAppState } from "../../../../state/StateProvider";
import Input from "../../../util/Input";
import Flex from "../../../util/layout/Flex";
import Grid from "../../../util/layout/Grid";
import Selector from "../../../util/menu/Selector";
import { useConfig } from "./Provider";
import { useAppState } from "../../../../../state/StateProvider";
import Input from "../../../../util/Input";
import Flex from "../../../../util/layout/Flex";
import Grid from "../../../../util/layout/Grid";
import Selector from "../../../../util/menu/Selector";
import { useConfig } from "../Provider";
const Git: Component<{}> = (p) => {
const { githubAccounts } = useAppState();
@@ -1,8 +1,8 @@
import { Component } from "solid-js";
import Input from "../../../util/Input";
import Flex from "../../../util/layout/Flex";
import Grid from "../../../util/layout/Grid";
import { useConfig } from "./Provider";
import Input from "../../../../util/Input";
import Flex from "../../../../util/layout/Flex";
import Grid from "../../../../util/layout/Grid";
import { useConfig } from "../Provider";
export const OnClone: Component<{}> = (p) => {
const { deployment, setDeployment } = useConfig();
@@ -1,8 +1,8 @@
import { Component } from "solid-js";
import Input from "../../../util/Input";
import Flex from "../../../util/layout/Flex";
import Grid from "../../../util/layout/Grid";
import { useConfig } from "./Provider";
import Input from "../../../../util/Input";
import Flex from "../../../../util/layout/Flex";
import Grid from "../../../../util/layout/Grid";
import { useConfig } from "../Provider";
const RepoMount: Component<{}> = (p) => {
const { deployment, setDeployment } = useConfig();
@@ -2,7 +2,7 @@ import { Component, createSignal, JSX } from "solid-js";
const ConfirmButton: Component<{
onConfirm?: () => void;
color?: "red" | "green" | "blue" | "orange";
color?: "red" | "green" | "blue" | "orange" | "grey";
style?: JSX.CSSProperties;
}> = (p) => {
const [confirm, set] = createSignal(false);
+2
View File
@@ -5,6 +5,7 @@ export const UPDATE_BUILD = "UPDATE_BUILD";
export const PULL_BUILD = "PULL_BUILD";
export const BUILD = "BUILD";
export const CLONE_BUILD_REPO = "CLONE_BUILD_REPO";
export const BUILD_OWNER_UPDATE = "BUILD_OWNER_UPDATE";
/* DEPLOY */
export const CREATE_DEPLOYMENT = "CREATE_DEPLOYMENT";
@@ -18,6 +19,7 @@ export const REFRESH_CONTAINER_STATUS = "REFRESH_CONTAINER_STATUS";
export const COPY_ENV = "COPY_ENV";
export const CLONE_DEPLOYMENT_REPO = "CLONE_DEPLOYMENT_REPO";
export const PULL_DEPLOYMENT = "PULL_DEPLOYMENT";
export const DEPLOYMENT_OWNER_UPDATE = "DEPLOYMENT_OWNER_UPDATE";
/* SERVER */
export const ADD_SERVER = "ADD_SERVER";
-1
View File
@@ -6,7 +6,6 @@ import {
onMount,
Setter,
} from "solid-js";
import { createStore, DeepReadonly, SetStoreFunction } from "solid-js/store";
export function useToggle(
initial = false
+16 -2
View File
@@ -67,6 +67,20 @@ export async function getDeploymentActionState(deploymentID: string) {
);
}
export async function addOwnerToDeployment(
deploymentID: string,
username: string
) {
return await client.post(`/api/deployment/${deploymentID}/${username}`);
}
export async function removeOwnerFromDeployment(
deploymentID: string,
username: string
) {
return await client.delete(`/api/deployment/${deploymentID}/${username}`);
}
export async function getServers() {
return await client.get<Servers>("/api/servers");
}
@@ -87,8 +101,8 @@ export async function getDockerAccounts() {
return await client.get<string[]>("/api/accounts/docker");
}
export async function getUsers() {
return await client.get<User[]>("/api/users");
export async function getUsers(username?: string) {
return await client.get<User[]>("/api/users" + generateQuery({ username }));
}
export async function updateUser(body: {
+2
View File
@@ -5,6 +5,7 @@ export const UPDATE_BUILD = "UPDATE_BUILD";
export const BUILD = "BUILD";
export const CLONE_BUILD_REPO = "CLONE_BUILD_REPO";
export const PULL_BUILD = "PULL_BUILD";
export const BUILD_OWNER_UPDATE = "BUILD_OWNER_UPDATE";
/* DEPLOY */
export const CREATE_DEPLOYMENT = "CREATE_DEPLOYMENT";
@@ -18,6 +19,7 @@ export const REFRESH_CONTAINER_STATUS = "REFRESH_CONTAINER_STATUS";
export const COPY_ENV = "COPY_ENV";
export const CLONE_DEPLOYMENT_REPO = "CLONE_DEPLOYMENT_REPO";
export const PULL_DEPLOYMENT = "PULL_DEPLOYMENT";
export const DEPLOYMENT_OWNER_UPDATE = "DEPLOYMENT_OWNER_UPDATE";
/* SERVER */
export const ADD_SERVER = "ADD_SERVER";