Just v2
This commit is contained in:
Andras Bacsai
2022-02-10 15:47:44 +01:00
committed by GitHub
parent a64b095c13
commit 460ae85226
403 changed files with 22039 additions and 12465 deletions

View File

@@ -0,0 +1,28 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params }) => {
const url = `/teams/${params.id}.json`;
const res = await fetch(url);
if (res.ok) {
const data = await res.json();
if (!data.permissions || Object.entries(data.permissions).length === 0) {
return {
status: 302,
redirect: '/teams'
};
}
return {
stuff: {
...data
}
};
}
return {
status: 302,
redirect: '/teams'
};
};
</script>
<slot />

View File

@@ -0,0 +1,55 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { userId, status, body } = await getUserDetails(event, false);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const user = await db.prisma.user.findFirst({
where: { id: userId, teams: { some: { id } } },
include: { permission: true }
});
if (!user) {
return {
status: 401
};
}
const permissions = await db.prisma.permission.findMany({
where: { teamId: id },
include: { user: { select: { id: true, email: true } } }
});
const team = await db.prisma.team.findUnique({ where: { id }, include: { permissions: true } });
const invitations = await db.prisma.teamInvitation.findMany({ where: { teamId: team.id } });
return {
body: {
team,
permissions,
invitations
}
};
} catch (error) {
return PrismaErrorHandler(error);
}
};
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
const { name } = await event.request.json();
try {
await db.prisma.team.update({ where: { id }, data: { name: { set: name } } });
return {
status: 201
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,214 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params }) => {
const url = `/teams/${params.id}.json`;
const res = await fetch(url);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
export let permissions;
export let team;
export let invitations;
import { page, session } from '$app/stores';
import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form';
import { post } from '$lib/api';
const { id } = $page.params;
let invitation = {
teamName: team.name,
email: null,
permission: 'read'
};
let myPermission = permissions.find((u) => u.user.id === $session.uid).permission;
function isAdmin(permission = myPermission) {
if (myPermission === 'admin' || myPermission === 'owner') {
return true;
}
return false;
}
async function sendInvitation() {
try {
await post(`/teams/${id}/invitation/invite.json`, {
teamId: team.id,
teamName: invitation.teamName,
email: invitation.email,
permission: invitation.permission
});
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
}
}
async function revokeInvitation(id) {
try {
await post(`/teams/${id}/invitation/revoke.json`, { id });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
}
}
async function removeFromTeam(uid) {
try {
await post(`/teams/${id}/remove/user.json`, { teamId: team.id, uid });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
}
}
async function changePermission(userId, permissionId, currentPermission) {
let newPermission = 'read';
if (currentPermission === 'read') {
newPermission = 'admin';
}
try {
await post(`/teams/${id}/permission/change.json`, { userId, newPermission, permissionId });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
}
}
async function handleSubmit() {
try {
await post(`/teams/${id}.json`, { ...team });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 px-6 text-2xl font-bold">
<div class="tracking-tight">Team</div>
<span class="arrow-right-applications px-1 text-cyan-500">></span>
<span class="pr-2">{team.name}</span>
</div>
<div class="mx-auto max-w-2xl">
<form on:submit|preventDefault={handleSubmit}>
<div class="flex space-x-1 p-6 font-bold">
<div class="title">Settings</div>
<div class="text-center">
<button class="bg-cyan-600 hover:bg-cyan-500" type="submit">Save</button>
</div>
</div>
<div class="mx-2 flex items-center space-x-2 px-4 sm:px-6">
<label for="name">Name</label>
<input id="name" name="name" placeholder="name" bind:value={team.name} />
</div>
{#if team.id === '0'}
<div class="px-20 pt-4 text-center">
<Explainer
maxWidthClass="w-full"
text="This is the <span class='text-red-500 font-bold'>root</span> team. <br><br>That means members of this group can manage instance wide settings and have all the priviliges in Coolify. (imagine like root user on Linux)"
/>
</div>
{/if}
</form>
<div class="flex space-x-1 py-5 px-6 pt-10 font-bold">
<div class="title">Members</div>
</div>
<div class="px-4 sm:px-6">
<table class="mx-2 w-full table-auto text-left">
<tr class="h-8 border-b border-coolgray-400">
<th scope="col">Email</th>
<th scope="col">Permission</th>
<th scope="col" class="text-center">Actions</th>
</tr>
{#each permissions as permission}
<tr class="text-xs">
<td class="py-4"
>{permission.user.email}
<span class="font-bold">{permission.user.id === $session.uid ? '(You)' : ''}</span></td
>
<td class="py-4">{permission.permission}</td>
{#if $session.isAdmin && permission.user.id !== $session.uid && permission.permission !== 'owner'}
<td class="flex flex-col items-center justify-center space-y-2 py-4 text-center">
<button
class="w-52 bg-red-600 hover:bg-red-500"
on:click={() => removeFromTeam(permission.user.id)}>Remove</button
>
<button
class="w-52"
on:click={() =>
changePermission(permission.user.id, permission.id, permission.permission)}
>Promote to {permission.permission === 'admin' ? 'read' : 'admin'}</button
>
</td>
{:else}
<td class="text-center py-4 flex-col space-y-2"> No actions available </td>
{/if}
</tr>
{/each}
{#each invitations as invitation}
<tr class="text-xs">
<td class="py-4 font-bold text-yellow-500">{invitation.email} </td>
<td class="py-4 font-bold text-yellow-500">{invitation.permission}</td>
{#if isAdmin(team.permissions[0].permission)}
<td class="flex-col space-y-2 py-4 text-center">
<button
class="w-52 bg-red-600 hover:bg-red-500"
on:click={() => revokeInvitation(invitation.id)}>Revoke invitation</button
>
</td>
{:else}
<td class="text-center py-4 flex-col space-y-2">Pending invitation</td>
{/if}
</tr>
{/each}
</table>
</div>
</div>
{#if $session.isAdmin}
<div class="mx-auto max-w-2xl pt-8">
<form on:submit|preventDefault={sendInvitation}>
<div class="flex space-x-1 p-6 font-bold">
<div class="title">Invite new member</div>
<div class="text-center">
<button class="bg-cyan-600 hover:bg-cyan-500" type="submit">Send invitation</button>
</div>
</div>
<div class="flex-col space-y-2 px-4 sm:px-6">
<div class="flex space-x-0">
<input
bind:value={invitation.email}
placeholder="Email address"
class="mr-2 w-full"
required
/>
<div class="flex-1" />
<button
on:click={() => (invitation.permission = 'read')}
class="rounded-none rounded-l"
type="button"
class:bg-pink-500={invitation.permission === 'read'}>Read</button
>
<button
on:click={() => (invitation.permission = 'admin')}
class="rounded-none rounded-r"
type="button"
class:bg-red-500={invitation.permission === 'admin'}>Admin</button
>
</div>
</div>
</form>
</div>
{/if}

View File

@@ -0,0 +1,36 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import { dayjs } from '$lib/dayjs';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { userId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = await event.request.json();
try {
const invitation = await db.prisma.teamInvitation.findFirst({
where: { uid: userId },
rejectOnNotFound: true
});
await db.prisma.team.update({
where: { id: invitation.teamId },
data: { users: { connect: { id: userId } } }
});
await db.prisma.permission.create({
data: {
user: { connect: { id: userId } },
permission: invitation.permission,
team: { connect: { id: invitation.teamId } }
}
});
await db.prisma.teamInvitation.delete({ where: { id } });
return {
status: 200
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,69 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import { dayjs } from '$lib/dayjs';
import type { RequestHandler } from '@sveltejs/kit';
async function createInvitation({ email, uid, teamId, teamName, permission }) {
return await db.prisma.teamInvitation.create({
data: { email, uid, teamId, teamName, permission }
});
}
export const post: RequestHandler = async (event) => {
const { userId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { email, permission, teamId, teamName } = await event.request.json();
try {
const userFound = await db.prisma.user.findUnique({ where: { email } });
if (!userFound) {
throw {
error: `No user found with '${email}' email address.`
};
}
const uid = userFound.id;
// Invitation to yourself?!
if (uid === userId) {
throw {
error: `Invitation to yourself? Whaaaaat?`
};
}
const alreadyInTeam = await db.prisma.team.findFirst({
where: { id: teamId, users: { some: { id: uid } } }
});
if (alreadyInTeam) {
throw {
error: `Already in the team.`
};
}
const invitationFound = await db.prisma.teamInvitation.findFirst({ where: { uid, teamId } });
if (invitationFound) {
if (dayjs().toDate() < dayjs(invitationFound.createdAt).add(1, 'day').toDate()) {
throw {
error: 'Invitiation already pending on user confirmation.'
};
} else {
await db.prisma.teamInvitation.delete({ where: { id: invitationFound.id } });
await createInvitation({ email, uid, teamId, teamName, permission });
return {
status: 200,
body: {
message: 'Invitiation sent.'
}
};
}
} else {
await createInvitation({ email, uid, teamId, teamName, permission });
return {
status: 200,
body: {
message: 'Invitiation sent.'
}
};
}
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,20 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import { dayjs } from '$lib/dayjs';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { userId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = await event.request.json();
try {
await db.prisma.teamInvitation.delete({ where: { id } });
return {
status: 200
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,23 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { userId, newPermission, permissionId } = await event.request.json();
try {
await db.prisma.permission.updateMany({
where: { id: permissionId, userId },
data: { permission: { set: newPermission } }
});
return {
status: 201
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,24 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { userId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { teamId, uid } = await event.request.json();
try {
await db.prisma.team.update({
where: { id: teamId },
data: { users: { disconnect: { id: uid } } }
});
await db.prisma.permission.deleteMany({ where: { userId: uid, teamId } });
return {
status: 201
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,26 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { PrismaErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { userId, status, body } = await getUserDetails(event, false);
if (status === 401) return { status, body };
try {
const teams = await db.prisma.permission.findMany({
where: { userId },
include: { team: { include: { _count: { select: { users: true } } } } }
});
const invitations = await db.prisma.teamInvitation.findMany({ where: { uid: userId } });
return {
status: 200,
body: {
teams,
invitations
}
};
} catch (error) {
return PrismaErrorHandler(error);
}
};

View File

@@ -0,0 +1,113 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch }) => {
const url = `/teams.json`;
const res = await fetch(url);
if (res.ok) {
return {
props: {
...(await res.json())
}
};
}
return {
status: res.status,
error: new Error(`Could not load ${url}`)
};
};
</script>
<script lang="ts">
import { errorNotification } from '$lib/form';
import { session } from '$app/stores';
import { post } from '$lib/api';
export let teams;
export let invitations;
async function acceptInvitation(id, teamId) {
try {
await post(`/teams/${teamId}/invitation/accept.json`, { id });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
}
}
async function revokeInvitation(id, teamId) {
try {
await post(`/teams/${teamId}/invitation/revoke.json`, { id });
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
}
}
</script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Teams</div>
{#if $session.isAdmin}
<a href="/new/team" class="add-icon bg-cyan-600 hover:bg-cyan-500">
<svg
class="w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</a>
{/if}
</div>
{#if invitations.length > 0}
<div class="mx-auto max-w-2xl pb-10">
<div class="flex space-x-1 p-6 font-bold">
<div class="title">Pending invitations</div>
</div>
<div class="text-center">
{#each invitations as invitation}
<div class="flex justify-center space-x-2">
<div>
Invited to <span class="font-bold text-pink-600">{invitation.teamName}</span> with
<span class="font-bold text-rose-600">{invitation.permission}</span> permission.
</div>
<button
class="hover:bg-green-500"
on:click={() => acceptInvitation(invitation.id, invitation.teamId)}>Accept</button
>
<button
class="hover:bg-red-600"
on:click={() => revokeInvitation(invitation.id, invitation.teamId)}>Delete</button
>
</div>
{/each}
</div>
</div>
{/if}
<div class="mx-auto max-w-2xl">
<div class="flex flex-wrap justify-center">
{#each teams as team}
<a href="/teams/{team.teamId}" class="w-96 p-2 no-underline">
<div
class="box-selection relative"
class:hover:bg-cyan-600={team.team?.id !== '0'}
class:hover:bg-red-500={team.team?.id === '0'}
>
<div class="truncate text-center text-xl font-bold">{team.team.name}</div>
<div class="text-center text-xs">
({team.team?.id === '0' ? 'root team - ' : ''}{team.permission})
</div>
<div class="mt-1 text-center">{team.team._count.users} member(s)</div>
</div>
</a>
{/each}
</div>
</div>