Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,23 @@ import {
} from "@thunderstore/thunderstore-api";
import { ApiAction } from "@thunderstore/ts-api-react-actions";

import { NotLoggedIn } from "app/commonComponents/NotLoggedIn/NotLoggedIn";
import { type OutletContextShape } from "app/root";
import { makeTeamSettingsTabLoader } from "cyberstorm/utils/dapperClientLoaders";
import { isTeamOwner } from "cyberstorm/utils/permissions";
import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm";

export const clientLoader = makeTeamSettingsTabLoader(
// TODO: add end point for checking can leave/disband status.
async (dapper, teamName) => ({ teamName })
async (dapper, teamName) => ({
permissions: dapper.getCurrentUserTeamPermissions(teamName),
})
);

export default function Settings() {
const { teamName } = useLoaderData<typeof clientLoader>();
const { permissions, teamName } = useLoaderData<typeof clientLoader>();
const outletContext = useOutletContext() as OutletContextShape;
const toast = useToast();
const navigate = useNavigate();

const currentUser = outletContext.currentUser?.username;
if (!currentUser) return <NotLoggedIn />;

async function moveToTeams() {
toast.addToast({
csVariant: "info",
Expand All @@ -56,56 +54,51 @@ export default function Settings() {

return (
<Suspense fallback={<div>Loading...</div>}>
<Await resolve={teamName}>
{(resolvedTeamName) => (
<Await resolve={permissions}>
{(resolvedPermissions) => (
<div className="settings-items">
<div className="settings-items__item">
<div className="settings-items__meta">
<p className="settings-items__title">Leave team</p>
<p className="settings-items__description">Leave your team</p>
<p className="settings-items__description">
Resign from the team
</p>
</div>
<div className="settings-items__content">
<NewAlert csVariant="danger">
You cannot currently leave this team as you are it&apos;s last
owner.
</NewAlert>
<p>
If you are the owner of the team, you can only leave if the
team has another owner assigned.
</p>
<LeaveTeamForm
userName={currentUser}
teamName={resolvedTeamName}
toast={toast}
config={outletContext.requestConfig}
updateTrigger={moveToTeams}
/>
{resolvedPermissions.can_leave_team ? (
<LeaveTeamForm
userName={outletContext.currentUser?.username ?? ""}
teamName={teamName}
toast={toast}
config={outletContext.requestConfig}
updateTrigger={moveToTeams}
/>
) : (
<LastOwnerAlert />
)}
</div>
</div>
<div className="settings-items__separator" />
<div className="settings-items__item">
<div className="settings-items__meta">
<p className="settings-items__title">Disband team</p>
<p className="settings-items__description">
Disband your team completely
Remove the team completely
</p>
</div>
<div className="settings-items__content">
<NewAlert csVariant="danger">
You cannot currently disband this team as it has packages.
</NewAlert>
<p>You are about to disband the team {resolvedTeamName}.</p>
<p>
Be aware you can currently only disband teams with no
packages. If you need to archive a team with existing pages,
contact Mythic#0001 on the Thunderstore Discord.
</p>
<DisbandTeamForm
teamName={resolvedTeamName}
updateTrigger={moveToTeams}
config={outletContext.requestConfig}
toast={toast}
/>
{resolvedPermissions.can_disband_team ? (
<DisbandTeamForm
teamName={teamName}
updateTrigger={moveToTeams}
config={outletContext.requestConfig}
toast={toast}
/>
) : isTeamOwner(teamName, outletContext.currentUser) ? (
<TeamHasPackagesAlert />
) : (
<NotTeamOwnerAlert />
)}
</div>
</div>
</div>
Expand All @@ -115,6 +108,28 @@ export default function Settings() {
);
}

const LastOwnerAlert = () => (
<NewAlert csVariant="info">
You cannot currently leave this team as you are its last owner.
<br />
To leave the team, you need to assign another owner to it. Alternatively,
you can disband the whole team.
</NewAlert>
);

const TeamHasPackagesAlert = () => (
<NewAlert csVariant="info">
You cannot currently disband this team as it has packages.
<br />
If you need to archive this team, contact #support in the{" "}
<a href="https://discord.thunderstore.io/">Thunderstore Discord</a>.
</NewAlert>
);

const NotTeamOwnerAlert = () => (
<NewAlert csVariant="info">Only team owners can disband teams.</NewAlert>
);

function LeaveTeamForm(props: {
userName: string;
teamName: string;
Expand Down Expand Up @@ -147,7 +162,7 @@ function LeaveTeamForm(props: {
<Modal
open={open}
onOpenChange={setOpen}
titleContent="Leave team"
titleContent="Leave team?"
csSize="small"
trigger={
<NewButton
Expand All @@ -170,7 +185,7 @@ function LeaveTeamForm(props: {
team={teamName}
csVariant="cyber"
>
{teamName}
{teamName}.
</NewLink>
</span>
</Modal.Body>
Expand Down Expand Up @@ -269,7 +284,7 @@ function DisbandTeamForm(props: {
<Modal
open={open}
onOpenChange={setOpen}
titleContent="Disband team"
titleContent="Disband team?"
csSize="small"
trigger={
<NewButton
Expand All @@ -292,7 +307,7 @@ function DisbandTeamForm(props: {
team={teamName}
csVariant="cyber"
>
{teamName}
{teamName}.
</NewLink>
</div>
<div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { assert, describe, it } from "vitest";

import type { CurrentUser } from "@thunderstore/dapper/types";

import { isTeamOwner } from "../permissions";

describe("utils.permissions.isTeamOwner", () => {
it("returns false if user is unauthenticated", () => {
const actual = isTeamOwner("test-team", undefined);

assert.isFalse(actual);
});

it("returns false if user does not belong to team", () => {
const user = {
teams_full: [{ name: "other-team", role: "owner", member_count: 1 }],
};

const actual = isTeamOwner("test-team", user as CurrentUser);

assert.isFalse(actual);
});

it("returns false if user is non-owner member", () => {
const user = {
teams_full: [{ name: "test-team", role: "member", member_count: 2 }],
};

const actual = isTeamOwner("test-team", user as CurrentUser);

assert.isFalse(actual);
});

it("returns true if user is owner of team", () => {
const user = {
teams_full: [
{ name: "other-team", role: "member", member_count: 1 },
{ name: "test-team", role: "owner", member_count: 3 },
],
};

const actual = isTeamOwner("test-team", user as CurrentUser);

assert.isTrue(actual);
});
});
7 changes: 7 additions & 0 deletions apps/cyberstorm-remix/cyberstorm/utils/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { CurrentUser } from "@thunderstore/dapper/types";

export const isTeamOwner = (
teamName: string,
currentUser: CurrentUser | undefined
) =>
currentUser?.teams_full?.find((t) => t.name === teamName)?.role === "owner";
9 changes: 9 additions & 0 deletions packages/dapper-fake/src/fakers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,12 @@ const getFakeOAuthConnection = (provider: string) => ({
username: faker.internet.userName(),
avatar: faker.helpers.maybe(getFakeImg) ?? null,
});

export const getFakeCurrentUserTeamPermissions = async (teamName: string) => {
setSeed(teamName);

return {
can_disband_team: faker.datatype.boolean(),
can_leave_team: faker.datatype.boolean(),
};
};
6 changes: 5 additions & 1 deletion packages/dapper-fake/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import {
getFakeTeamMembers,
postFakeTeamCreate,
} from "./fakers/team";
import { getFakeCurrentUser } from "./fakers/user";
import {
getFakeCurrentUser,
getFakeCurrentUserTeamPermissions,
} from "./fakers/user";
import { postFakePackageSubmissionMetadata } from "./fakers/submission";
import { getFakePackageSubmissionStatus } from "./fakers/submission";

Expand All @@ -30,6 +33,7 @@ export class DapperFake implements DapperInterface {
public getCommunity = getFakeCommunity;
public getCommunityFilters = getFakeCommunityFilters;
public getCurrentUser = getFakeCurrentUser;
public getCurrentUserTeamPermissions = getFakeCurrentUserTeamPermissions;
public getPackageChangelog = getFakeChangelog;
public getPackagePermissions = getFakePackagePermissions;
public getPackageListingDetails = getFakePackageListingDetails;
Expand Down
8 changes: 7 additions & 1 deletion packages/dapper-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { getDynamicHTML } from "./methods/dynamicHTML";
import { getCommunities, getCommunity } from "./methods/communities";
import { getCommunityFilters } from "./methods/communityFilters";
import { getRatedPackages } from "./methods/ratedPackages";
import { getCurrentUser } from "./methods/currentUser";
import {
getCurrentUser,
getCurrentUserTeamPermissions,
} from "./methods/currentUser";
import {
getPackageChangelog,
getPackageReadme,
Expand Down Expand Up @@ -48,6 +51,8 @@ export class DapperTs implements DapperTsInterface {
this.getCommunityFilters = this.getCommunityFilters.bind(this);
this.getRatedPackages = this.getRatedPackages.bind(this);
this.getCurrentUser = this.getCurrentUser.bind(this);
this.getCurrentUserTeamPermissions =
this.getCurrentUserTeamPermissions.bind(this);
this.getPackageChangelog = this.getPackageChangelog.bind(this);
this.getPackageListings = this.getPackageListings.bind(this);
this.getPackageListingDetails = this.getPackageListingDetails.bind(this);
Expand Down Expand Up @@ -76,6 +81,7 @@ export class DapperTs implements DapperTsInterface {
public getCommunityFilters = getCommunityFilters;
public getRatedPackages = getRatedPackages;
public getCurrentUser = getCurrentUser;
public getCurrentUserTeamPermissions = getCurrentUserTeamPermissions;
public getPackageChangelog = getPackageChangelog;
public getPackageListings = getPackageListings;
public getPackageListingDetails = getPackageListingDetails;
Expand Down
18 changes: 17 additions & 1 deletion packages/dapper-ts/src/methods/currentUser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ApiError, fetchCurrentUser } from "@thunderstore/thunderstore-api";
import {
ApiError,
fetchCurrentUser,
fetchCurrentUserTeamPermissions,
} from "@thunderstore/thunderstore-api";

import { DapperTsInterface } from "../index";

Expand All @@ -22,3 +26,15 @@ export async function getCurrentUser(this: DapperTsInterface) {
}
}
}

export async function getCurrentUserTeamPermissions(
this: DapperTsInterface,
teamName: string
) {
return await fetchCurrentUserTeamPermissions({
config: this.config,
params: { team_name: teamName },
data: {},
queryParams: {},
});
}
1 change: 1 addition & 0 deletions packages/dapper/src/dapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface DapperInterface {
getCommunity: methods.GetCommunity;
getCommunityFilters: methods.GetCommunityFilters;
getCurrentUser: methods.GetCurrentUser;
getCurrentUserTeamPermissions: methods.GetCurrentUserTeamPermissions;
getPackageChangelog: methods.GetPackageChangelog;
getPackageListingDetails: methods.GetPackageListingDetails;
getPackageListings: methods.GetPackageListings;
Expand Down
6 changes: 5 additions & 1 deletion packages/dapper/src/types/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { type PackageListingType } from "./props";
import { type HTMLContentResponse, type MarkdownResponse } from "./shared";
import { type TeamDetails, type ServiceAccount, type TeamMember } from "./team";
import { type CurrentUser } from "./user";
import { type CurrentUser, type CurrentUserTeamPermissions } from "./user";

export type GetCommunities = (
page?: number,
Expand All @@ -31,6 +31,10 @@ export type GetCommunityFilters = (

export type GetCurrentUser = () => Promise<null | CurrentUser>;

export type GetCurrentUserTeamPermissions = (
teamName: string
) => Promise<CurrentUserTeamPermissions>;

export type GetPackageChangelog = (
namespace: string,
name: string,
Expand Down
5 changes: 5 additions & 0 deletions packages/dapper/src/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,8 @@ interface Badge {
description: string;
imageSource: string;
}

export interface CurrentUserTeamPermissions {
can_disband_team: boolean;
can_leave_team: boolean;
}
25 changes: 25 additions & 0 deletions packages/thunderstore-api/src/get/currentUser.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { ApiEndpointProps } from "../index";
import { apiFetch } from "../apiFetch";
import {
type CurrentUserTeamPermissionsRequestParams,
currentUserTeamPermissionsRequestParamsSchema,
} from "../schemas/requestSchemas";
import {
CurrentUserResponseData,
currentUserResponseDataSchema,
CurrentUserTeamPermissionsResponseData,
currentUserTeamPermissionsResponseDataSchema,
} from "../schemas/responseSchemas";

export async function fetchCurrentUser(
Expand All @@ -19,3 +25,22 @@ export async function fetchCurrentUser(
responseSchema: currentUserResponseDataSchema,
});
}

export async function fetchCurrentUserTeamPermissions(
props: ApiEndpointProps<
CurrentUserTeamPermissionsRequestParams,
object,
object
>
): Promise<CurrentUserTeamPermissionsResponseData> {
const { config, params } = props;
const path = `api/experimental/current-user/permissions/team/${params.team_name}/`;
const request = { cache: "no-store" as RequestCache };

return await apiFetch({
args: { config, path, request, useSession: true },
requestSchema: currentUserTeamPermissionsRequestParamsSchema,
queryParamsSchema: undefined,
responseSchema: currentUserTeamPermissionsResponseDataSchema,
});
}
Loading
Loading