Edit badge owners button with permissions

This commit is contained in:
Kalle
2022-07-05 15:46:00 +03:00
parent 94d5e8c137
commit 80b901a121
6 changed files with 99 additions and 34 deletions

View File

@@ -39,7 +39,7 @@ export function all() {
return allStm.all() as All;
}
export type OwnersByBadge = Array<
export type OwnersByBadgeId = Array<
Pick<User, "id" | "discordId" | "discordName" | "discordDiscriminator"> & {
count: number;
}
@@ -47,18 +47,37 @@ export type OwnersByBadge = Array<
const ownersByBadgeIdStm = sql.prepare(`
select
count("BadgeOwner"."badgeId") as count,
"User"."id",
"User"."discordId",
"User"."discordName",
"User"."discordDiscriminator"
from "BadgeOwner"
join "User" on "User"."id" = "BadgeOwner"."userId"
where "BadgeOwner"."badgeId" = $id
group by "User"."id"
order by count desc
count("BadgeOwner"."badgeId") as count,
"User"."id",
"User"."discordId",
"User"."discordName",
"User"."discordDiscriminator"
from "BadgeOwner"
join "User" on "User"."id" = "BadgeOwner"."userId"
where "BadgeOwner"."badgeId" = $id
group by "User"."id"
order by count desc
`);
export function ownersByBadgeId(id: Badge["id"]) {
return ownersByBadgeIdStm.all({ id }) as OwnersByBadge;
return ownersByBadgeIdStm.all({ id }) as OwnersByBadgeId;
}
export type ManagersByBadgeId = Array<
Pick<User, "id" | "discordId" | "discordName" | "discordDiscriminator">
>;
const managersByBadgeIdStm = sql.prepare(`
select
"User"."id",
"User"."discordId",
"User"."discordName",
"User"."discordDiscriminator"
from "BadgeManager"
join "User" on "User"."id" = "BadgeManager"."userId"
where "BadgeManager"."badgeId" = $id
`);
export function managersByBadgeId(id: Badge["id"]) {
return managersByBadgeIdStm.all({ id }) as ManagersByBadgeId;
}

View File

@@ -25,6 +25,7 @@ const basicSeeds = [
thisMonthsSuggestions,
badgesToAdmin,
badgesToUsers,
badgeManagers,
];
export function seed() {
@@ -258,3 +259,14 @@ function badgesToUsers() {
}
}
}
function badgeManagers() {
// make N-ZAP user manager of several badges
for (let id = 1; id <= 10; id++) {
sql
.prepare(
`insert into "BadgeManager" ("badgeId", "userId") values ($id, $userId)`
)
.run({ id, userId: 2 });
}
}

View File

@@ -4,9 +4,26 @@ import type { PlusSuggestion, User, UserWithPlusTier } from "./db/types";
import { allTruthy } from "./utils/arrays";
import { ADMIN_DISCORD_ID, LOHI_TOKEN_HEADER_NAME } from "./constants";
import invariant from "tiny-invariant";
import type { ManagersByBadgeId } from "./db/models/badges.server";
// TODO: 1) move "root checkers" to one file and utils to one file 2) make utils const for more terseness
export function canPerformAdminActions(user?: Pick<User, "discordId">) {
if (["development", "test"].includes(process.env.NODE_ENV)) return true;
if (!user) return false;
return user.discordId === ADMIN_DISCORD_ID;
}
function adminOverride(user?: Pick<User, "discordId">) {
if (canPerformAdminActions(user)) {
return () => true;
}
return (canPerformActionAsNormalUser: boolean) =>
canPerformActionAsNormalUser;
}
interface CanAddCommentToSuggestionArgs {
user?: Pick<UserWithPlusTier, "id" | "plusTier">;
suggestions: plusSuggestions.FindVisibleForUser;
@@ -205,16 +222,26 @@ function hasUserSuggestedThisMonth({
);
}
export function canPerformAdminActions(user?: Pick<User, "discordId">) {
if (["development", "test"].includes(process.env.NODE_ENV)) return true;
if (!user) return false;
return user.discordId === ADMIN_DISCORD_ID;
}
export function canAccessLohiEndpoint(request: Request) {
invariant(process.env["LOHI_TOKEN"], "LOHI_TOKEN is required");
return (
request.headers.get(LOHI_TOKEN_HEADER_NAME) === process.env["LOHI_TOKEN"]
);
}
interface CanEditBadgeOwnersArgs {
user?: Pick<User, "id" | "discordId">;
managers: ManagersByBadgeId;
}
export function canEditBadgeOwners({ user, managers }: CanEditBadgeOwnersArgs) {
return adminOverride(user)(isBadgeManager({ user, managers }));
}
function isBadgeManager({
user,
managers,
}: Pick<CanEditBadgeOwnersArgs, "user" | "managers">) {
if (!user) return false;
return managers.some((manager) => manager.id === user.id);
}

View File

@@ -1,3 +1,4 @@
import { json } from "@remix-run/node";
import type { LinksFunction, LoaderFunction } from "@remix-run/node";
import { NavLink, Outlet, useLoaderData } from "@remix-run/react";
import { Badge } from "~/components/Badge";
@@ -5,7 +6,6 @@ import { Main } from "~/components/Main";
import { db } from "~/db";
import type { All } from "~/db/models/badges.server";
import styles from "~/styles/badges.css";
import { jsonCached } from "~/utils/remix";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];
@@ -16,7 +16,7 @@ export interface BadgesLoaderData {
}
export const loader: LoaderFunction = () => {
return jsonCached<BadgesLoaderData>({ badges: db.badges.all() }, 120);
return json<BadgesLoaderData>({ badges: db.badges.all() });
};
export default function BadgesPageLayout() {

View File

@@ -1,18 +1,25 @@
import { json } from "@remix-run/node";
import type { LoaderFunction } from "@remix-run/node";
import { useLoaderData, useMatches, useParams } from "@remix-run/react";
import clsx from "clsx";
import { Badge } from "~/components/Badge";
import { LinkButton } from "~/components/Button";
import { Redirect } from "~/components/Redirect";
import { db } from "~/db";
import type { OwnersByBadge } from "~/db/models/badges.server";
import type {
ManagersByBadgeId,
OwnersByBadgeId,
} from "~/db/models/badges.server";
import type { Badge as BadgeDBType } from "~/db/types";
import { jsonCached } from "~/utils/remix";
import { useUser } from "~/modules/auth";
import { canEditBadgeOwners } from "~/permissions";
import { discordFullName } from "~/utils/strings";
import { BADGES_PAGE } from "~/utils/urls";
import type { BadgesLoaderData } from "../badges";
export interface BadgeDetailsLoaderData {
owners: OwnersByBadge;
owners: OwnersByBadgeId;
managers: ManagersByBadgeId;
}
export const loader: LoaderFunction = ({ params }) => {
@@ -21,13 +28,14 @@ export const loader: LoaderFunction = ({ params }) => {
throw new Response(null, { status: 404 });
}
return jsonCached<BadgeDetailsLoaderData>(
{ owners: db.badges.ownersByBadgeId(badgeId) },
120
);
return json<BadgeDetailsLoaderData>({
owners: db.badges.ownersByBadgeId(badgeId),
managers: db.badges.managersByBadgeId(badgeId),
});
};
export default function BadgeDetailsPage() {
const user = useUser();
const [, parentRoute] = useMatches();
const { badges } = parentRoute!.data as BadgesLoaderData;
const params = useParams();
@@ -40,6 +48,11 @@ export default function BadgeDetailsPage() {
<div className="stack md items-center">
<Badge badge={badge} isAnimated size={200} />
<div className="badges__explanation">{badgeExplanationText(badge)}</div>
{canEditBadgeOwners({ user, managers: data.managers }) ? (
<LinkButton to="edit" variant="outlined" tiny>
Edit
</LinkButton>
) : null}
<div className="badges__owners-container">
<ul className="badges__owners">
{data.owners.map((owner) => (

View File

@@ -50,9 +50,3 @@ export function validate(condition: any): asserts condition {
export function makeTitle(title: string) {
return `${title} | sendou.ink`;
}
export function jsonCached<T>(data: T, cachedForInMinutes: number) {
return json(data, {
headers: { "Cache-Control": `max-age=${cachedForInMinutes * 60}` },
});
}