Boom
This commit is contained in:
parent
32068b3b19
commit
ccb6f810d6
6 changed files with 88 additions and 23 deletions
|
@ -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",
|
||||
|
|
25
db/mod.ts
25
db/mod.ts
|
@ -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> {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
8
types.ts
8
types.ts
|
@ -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>;
|
||||
|
|
Loading…
Reference in a new issue