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