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": { "team_user": {
prepStatements: [ prepStatements: [
"drop type if exists team_user_status", "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: [ columns: [
"team_id uuid", "team_id uuid",

View file

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

View file

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

View file

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

View file

@ -1,13 +1,20 @@
import { Handlers, PageProps } from "$fresh/server.ts"; import { Handlers, PageProps } from "$fresh/server.ts";
import { getTeam, getTeamUsers, isUserInTeam } from "@/db/mod.ts"; import { getTeam, getTeamUsers, teamUserStatus } from "@/db/mod.ts";
import { type Team, type User } from "@/types.ts"; import { type Team, type TeamUserStatus, type User } from "@/types.ts";
import { type ContextState } from "@/types.ts"; import { type ContextState } from "@/types.ts";
interface TeamPageProps { interface TeamIndexProps {
status: TeamUserStatus;
team: Team; team: Team;
users: User[]; users: User[];
} }
interface TeamStatusProps {
status: TeamUserStatus;
}
type TeamPageProps = TeamIndexProps | TeamStatusProps;
export const handler: Handlers<TeamPageProps, ContextState> = { export const handler: Handlers<TeamPageProps, ContextState> = {
async GET(request, context) { async GET(request, context) {
if (!context.state.user?.id) { if (!context.state.user?.id) {
@ -23,13 +30,19 @@ export const handler: Handlers<TeamPageProps, ContextState> = {
console.debug({ request, context }); console.debug({ request, context });
try { try {
if (!await isUserInTeam(context.state.user?.id, id)) { const status = await teamUserStatus(context.state.user?.id, id);
// users that are not a member of a team may not view it console.log("Status of this user on team:", status, id);
if (!status) {
return await context.renderNotFound(); 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 }); return await context.renderNotFound();
const users = await getTeamUsers(team) || [];
return await context.render({ team, users });
} catch (e) { } catch (e) {
console.error(`Error handling team page for ID '${id}'`, e); console.error(`Error handling team page for ID '${id}'`, e);
return await context.renderNotFound(); return await context.renderNotFound();
@ -37,15 +50,27 @@ export const handler: Handlers<TeamPageProps, ContextState> = {
}, },
}; };
export default function Team( export default function TeamPage({ data }: PageProps<TeamPageProps>) {
{ data: { team: { createdAt, displayName }, users } }: PageProps< if ("users" in data) {
TeamPageProps 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 ( return (
<> <>
<a href="/dashboard">Back to dashboard</a> <a href="/dashboard">Back to dashboard</a>
<h1>{displayName} - created {createdAt.toLocaleString()}</h1> <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> <h1 class="mt-4">Team Members</h1>
<ul> <ul>
{users.map((user) => ( {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; something?: string;
} }
export type TeamUserStatus = "invited" | "accepted" | "owner"; export type TeamUserStatus =
| "invited"
| "accepted"
| "manager"
| "owner"
| "removed"
| "left";
export interface TeamUser { export interface TeamUser {
userId: IdentifierFor<User>; userId: IdentifierFor<User>;
teamId: IdentifierFor<Team>; teamId: IdentifierFor<Team>;