mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-13 14:46:10 -05:00
Edit badge owners button with permissions
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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}` },
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user