First authz check
This commit is contained in:
parent
773439d2e0
commit
e667f39852
54
db/mod.ts
54
db/mod.ts
|
@ -38,6 +38,36 @@ export function initDatabaseConnectionPool({ url }: PostgresConfig) {
|
||||||
testDbConnection();
|
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[],
|
||||||
|
): Promise<boolean> {
|
||||||
|
const result = await queryArray<[boolean]>(
|
||||||
|
`select exists(select 1 from ${sqlSnippet});`,
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
if (result && result.rows.length > 0) {
|
||||||
|
log.info("rowExists result:", result.rows);
|
||||||
|
return !!(result.rows[0][0]);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isUserInTeam(
|
||||||
|
userId: string,
|
||||||
|
teamId: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return await rowExists("team_user where user_id = $1 and team_id = $2", [
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
export async function testDbConnection(): Promise<boolean> {
|
export async function testDbConnection(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await dbOp((conn) => conn.queryObject("select 1"));
|
await dbOp((conn) => conn.queryObject("select 1"));
|
||||||
|
@ -111,17 +141,20 @@ export async function dbOp<T>(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example: queryObject('select * from "user" where id = $1', [userId])
|
||||||
|
*/
|
||||||
export async function queryObject<T>(
|
export async function queryObject<T>(
|
||||||
sql: string,
|
sql: string,
|
||||||
args?: QueryArguments,
|
args?: QueryArguments,
|
||||||
connection?: PoolClient | Transaction,
|
connection?: PoolClient | Transaction,
|
||||||
): Promise<QueryObjectResult<T> | null> {
|
): Promise<QueryObjectResult<T> | null> {
|
||||||
console.debug(`queryObject: ${sql}`);
|
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
return await dbOp(async (connection) => {
|
return await dbOp(async (connection) => {
|
||||||
return await queryObject(sql, args, connection);
|
return await queryObject(sql, args, connection);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
log.debug(`queryObject: ${sql}`);
|
||||||
const result = await connection.queryObject<T>({
|
const result = await connection.queryObject<T>({
|
||||||
camelcase: true,
|
camelcase: true,
|
||||||
text: sql.trim(),
|
text: sql.trim(),
|
||||||
|
@ -132,17 +165,20 @@ export async function queryObject<T>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function queryArray<T extends []>(
|
/**
|
||||||
|
* Example: queryArray('select * from "user" where id = $1', [userId])
|
||||||
|
*/
|
||||||
|
export async function queryArray<T extends unknown[]>(
|
||||||
sql: string,
|
sql: string,
|
||||||
args?: QueryArguments,
|
args?: QueryArguments,
|
||||||
connection?: PoolClient,
|
connection?: PoolClient,
|
||||||
): Promise<QueryArrayResult<T> | null> {
|
): Promise<QueryArrayResult<T> | null> {
|
||||||
console.debug(`queryArray: ${sql}`);
|
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
return await dbOp(async (connection) => {
|
return await dbOp(async (connection) => {
|
||||||
return await queryArray<T>(sql, args, connection);
|
return await queryArray<T>(sql, args, connection);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
log.debug(`queryArray: ${sql}`);
|
||||||
const result = await connection.queryArray<T>({
|
const result = await connection.queryArray<T>({
|
||||||
text: sql.trim(),
|
text: sql.trim(),
|
||||||
args,
|
args,
|
||||||
|
@ -210,7 +246,7 @@ export async function createTeam(
|
||||||
},
|
},
|
||||||
transaction?: Transaction,
|
transaction?: Transaction,
|
||||||
): Promise<Team> {
|
): Promise<Team> {
|
||||||
console.debug("createTeam tx:", transaction);
|
log.debug("createTeam tx:", transaction);
|
||||||
if (!transaction) {
|
if (!transaction) {
|
||||||
return await wrapWithTransaction<Team>(
|
return await wrapWithTransaction<Team>(
|
||||||
"createTeam",
|
"createTeam",
|
||||||
|
@ -234,7 +270,7 @@ export async function createTeam(
|
||||||
}
|
}
|
||||||
return team;
|
return team;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error creating team:", e);
|
log.error("Error creating team:", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -253,7 +289,7 @@ export async function wrapWithTransaction<T>(
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await transaction.begin();
|
await transaction.begin();
|
||||||
console.debug(
|
log.debug(
|
||||||
`started ${transactionName} tx with options ${
|
`started ${transactionName} tx with options ${
|
||||||
JSON.stringify(transactionOptions)
|
JSON.stringify(transactionOptions)
|
||||||
}:`,
|
}:`,
|
||||||
|
@ -264,11 +300,11 @@ export async function wrapWithTransaction<T>(
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
console.error("Failed to complete transaction:", e);
|
log.error("Failed to complete transaction:", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to create transaction");
|
log.error("Failed to create transaction");
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -304,7 +340,7 @@ export async function createUser(
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error creating user:", e);
|
log.error("Error creating user:", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,31 @@
|
||||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
import { getTeam, getTeamUsers } from "@/db/mod.ts";
|
import { getTeam, getTeamUsers, isUserInTeam } from "@/db/mod.ts";
|
||||||
import { type Team, type User } from "@/types.ts";
|
import { type Team, type User } from "@/types.ts";
|
||||||
|
import { type ContextState } from "@/types.ts";
|
||||||
|
|
||||||
interface TeamPageProps {
|
interface TeamPageProps {
|
||||||
team: Team;
|
team: Team;
|
||||||
users: User[];
|
users: User[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handler: Handlers<TeamPageProps> = {
|
export const handler: Handlers<TeamPageProps, ContextState> = {
|
||||||
async GET(request, context) {
|
async GET(request, context) {
|
||||||
|
if (!context.state.user?.id) {
|
||||||
|
// unauthenticated requests may not view teams
|
||||||
|
return await context.renderNotFound();
|
||||||
|
}
|
||||||
|
// TODO: implement this with row-level security?
|
||||||
|
// TODO: do I just use supabase at this point?
|
||||||
// TODO: only allow logged-in users to view teams (and most resources!)
|
// TODO: only allow logged-in users to view teams (and most resources!)
|
||||||
// TODO: only allow users that are a member of a team to view them
|
// TODO: only allow users that are a member of a team to view them
|
||||||
// NOTE: maybe teams can be public...?
|
// NOTE: maybe teams can be public...?
|
||||||
const { id } = context.params;
|
const { id } = context.params;
|
||||||
|
|
||||||
|
if (!await isUserInTeam(context.state.user?.id, id)) {
|
||||||
|
// users that are not a member of a team may not view it
|
||||||
|
return await context.renderNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
console.debug({ request, context });
|
console.debug({ request, context });
|
||||||
try {
|
try {
|
||||||
const team = await getTeam({ id });
|
const team = await getTeam({ id });
|
||||||
|
|
Loading…
Reference in a new issue