This commit is contained in:
Daniel Flanagan 2022-11-10 16:39:48 -06:00
parent 32068b3b19
commit ccb6f810d6
Signed by: lytedev
GPG key ID: 5B2020A0F9921EF4
6 changed files with 88 additions and 23 deletions

View file

@ -84,7 +84,7 @@ const tables: Record<string, TableSpec> = {
"team_user": {
prepStatements: [
"drop type if exists team_user_status",
"create type team_user_status as enum ('invited', 'accepted', 'owner')",
"create type team_user_status as enum ('invited', 'accepted', 'manager', 'owner', 'removed', 'left')",
],
columns: [
"team_id uuid",

View file

@ -10,6 +10,7 @@ import {
type QueryArrayResult,
type QueryObjectResult,
} from "https://deno.land/x/postgres@v0.16.1/query/query.ts?s=QueryArguments";
import { type TeamUserStatus } from "@/types.ts";
import * as base64 from "$std/encoding/base64.ts";
import { log } from "@/log.ts";
@ -38,11 +39,12 @@ export function initDatabaseConnectionPool({ url }: PostgresConfig) {
testDbConnection();
}
/**
/*
* Checks that a certain SQL predicate fetches a row that indeed exists.
*
* sqlSnippet should assume it comes after a 'select * from'. For example: '"user" where id = 1'.
*/
/*
async function rowExists(
sqlSnippet: string,
args: unknown[],
@ -56,15 +58,24 @@ async function rowExists(
}
return false;
}
*/
export async function isUserInTeam(
export async function teamUserStatus(
userId: string,
teamId: string,
): Promise<boolean> {
return await rowExists("team_user where user_id = $1 and team_id = $2", [
userId,
teamId,
]);
): Promise<TeamUserStatus | undefined> {
try {
const result = await queryObject<{ status: TeamUserStatus }>(
"select status from team_user where user_id = $1 and team_id = $2",
[
userId,
teamId,
],
);
return result?.rows[0].status;
} catch (_e) {
return undefined;
}
}
export async function testDbConnection(): Promise<boolean> {

View file

@ -40,7 +40,6 @@ export function UserNavItems() {
export default function App(
{ Component, contextState }: AppProps<ContextState>,
) {
console.log("contextState", contextState);
return (
<div class="relative min-h-screen flex flex-col">
<header class="flex justify-start items-center">

View file

@ -34,7 +34,6 @@ async function currentUser(
hasBadAuthCookie = true;
}
}
log.info(context.state);
const resp = await context.next();
if (resp && hasBadAuthCookie) {
deleteCookie(resp.headers, "lsauth");

View file

@ -1,13 +1,20 @@
import { Handlers, PageProps } from "$fresh/server.ts";
import { getTeam, getTeamUsers, isUserInTeam } from "@/db/mod.ts";
import { type Team, type User } from "@/types.ts";
import { getTeam, getTeamUsers, teamUserStatus } from "@/db/mod.ts";
import { type Team, type TeamUserStatus, type User } from "@/types.ts";
import { type ContextState } from "@/types.ts";
interface TeamPageProps {
interface TeamIndexProps {
status: TeamUserStatus;
team: Team;
users: User[];
}
interface TeamStatusProps {
status: TeamUserStatus;
}
type TeamPageProps = TeamIndexProps | TeamStatusProps;
export const handler: Handlers<TeamPageProps, ContextState> = {
async GET(request, context) {
if (!context.state.user?.id) {
@ -23,13 +30,19 @@ export const handler: Handlers<TeamPageProps, ContextState> = {
console.debug({ request, context });
try {
if (!await isUserInTeam(context.state.user?.id, id)) {
// users that are not a member of a team may not view it
const status = await teamUserStatus(context.state.user?.id, id);
console.log("Status of this user on team:", status, id);
if (!status) {
return await context.renderNotFound();
} else if (["accepted", "manager", "owner"].includes(status)) {
// users that are not a member of a team may not view it
const team = await getTeam({ id });
const users = await getTeamUsers(team) || [];
return await context.render({ team, users, status });
} else if (["invited", "left", "removed"].includes(status)) {
return await context.render({ status });
}
const team = await getTeam({ id });
const users = await getTeamUsers(team) || [];
return await context.render({ team, users });
return await context.renderNotFound();
} catch (e) {
console.error(`Error handling team page for ID '${id}'`, e);
return await context.renderNotFound();
@ -37,15 +50,27 @@ export const handler: Handlers<TeamPageProps, ContextState> = {
},
};
export default function Team(
{ data: { team: { createdAt, displayName }, users } }: PageProps<
TeamPageProps
>,
export default function TeamPage({ data }: PageProps<TeamPageProps>) {
if ("users" in data) {
return <TeamIndex {...data}></TeamIndex>;
} else {
return <TeamStatus {...data}></TeamStatus>;
}
}
function TeamStatus({ status }: TeamStatusProps) {
return <>Team status: {status}</>;
}
function TeamIndex(
{ team: { id, displayName, createdAt }, users, status }: TeamIndexProps,
) {
return (
<>
<a href="/dashboard">Back to dashboard</a>
<h1>{displayName} - created {createdAt.toLocaleString()}</h1>
{/* <h1 class="mt-4">Administrate</h1> */}
{["owner", "manager"].includes(status) && <Manage teamId={id} />}
<h1 class="mt-4">Team Members</h1>
<ul>
{users.map((user) => (
@ -59,3 +84,28 @@ export default function Team(
</>
);
}
function Manage({ teamId }: { teamId: string }) {
return (
<>
<h1 class="mt-4">Manage</h1>
<section>
<h2>Invite User</h2>
<form
autocomplete="off"
method="post"
action={`/team/${teamId}/invite`}
>
<input
autocomplete="off"
type="text"
placeholder="Username"
name="inviteUsername"
id="inviteUsername"
/>
<input type="submit" value="Invite" />
</form>
</section>
</>
);
}

View file

@ -53,7 +53,13 @@ export interface ContextState extends Record<string, unknown> {
something?: string;
}
export type TeamUserStatus = "invited" | "accepted" | "owner";
export type TeamUserStatus =
| "invited"
| "accepted"
| "manager"
| "owner"
| "removed"
| "left";
export interface TeamUser {
userId: IdentifierFor<User>;
teamId: IdentifierFor<Team>;