Skip to content

Commit

Permalink
Admin and moderator functionality (#412)
Browse files Browse the repository at this point in the history
  • Loading branch information
Winston-Hsiao authored Sep 19, 2024
1 parent 62e0679 commit 3ab2565
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 17 deletions.
7 changes: 1 addition & 6 deletions frontend/src/components/auth/SignupForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,7 @@ const SignupForm: React.FC<SignupFormProps> = ({ signupTokenId }) => {
.
</div>
{/* Signup Button */}
<Button
variant="outline"
className="w-full text-white bg-blue-600 hover:bg-opacity-70"
>
Sign up
</Button>
<Button variant="primary">Sign up</Button>
</form>
);
};
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/components/pages/EmailSignup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ const EmailSignup = () => {
<div className="text-center">
<p className="text-lg mb-8">Invalid Sign Up Link</p>
<Button
variant="outline"
className="w-full text-white bg-blue-600 hover:bg-opacity-70"
variant="primary"
onClick={() => {
navigate("/login");
}}
Expand Down
34 changes: 33 additions & 1 deletion frontend/src/components/pages/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ interface RenderProfileProps {
onUpdateProfile: (updatedUser: Partial<UserResponse>) => Promise<void>;
canEdit: boolean;
listingIds: string[] | null;
isAdmin: boolean;
}

const RenderProfile = (props: RenderProfileProps) => {
const navigate = useNavigate();
const { user, onUpdateProfile, canEdit, listingIds } = props;
const auth = useAuthentication();
const { user, onUpdateProfile, canEdit, listingIds, isAdmin } = props;
const [isEditing, setIsEditing] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [firstName, setFirstName] = useState(user.first_name || "");
Expand Down Expand Up @@ -52,6 +54,20 @@ const RenderProfile = (props: RenderProfileProps) => {
}
};

const handleSetModerator = async () => {
try {
await auth.client.POST("/users/set-moderator", {
body: {
user_id: user.id,
is_mod: !user.permissions?.includes("is_mod"),
},
});
window.location.reload();
} catch (error) {
console.error("Failed to set moderator", error);
}
};

return (
<div className="space-y-8 mb-12">
<Card className="w-full max-w-4xl mx-auto">
Expand All @@ -75,6 +91,13 @@ const RenderProfile = (props: RenderProfileProps) => {
</Button>
</div>
)}
{isAdmin && (
<Button onClick={handleSetModerator} variant="outline">
{user.permissions?.includes("is_mod")
? "Remove Moderator"
: "Set as Moderator"}
</Button>
)}
</CardHeader>
<CardContent>
{isEditing ? (
Expand Down Expand Up @@ -181,6 +204,7 @@ const Profile = () => {
const [canEdit, setCanEdit] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [listingIds, setListingIds] = useState<string[] | null>(null);
const [isAdmin, setIsAdmin] = useState(false);

const pageNumber = parseInt("1", 10);

Expand All @@ -191,6 +215,9 @@ const Profile = () => {
if (auth.currentUser) {
setUser(auth.currentUser);
setCanEdit(true);
setIsAdmin(
auth.currentUser.permissions?.includes("is_admin") || false,
);
setIsLoading(false);
} else if (!auth.isLoading) {
const { data, error } = await auth.client.GET("/users/public/me");
Expand All @@ -199,6 +226,7 @@ const Profile = () => {
} else {
setUser(data);
setCanEdit(true);
setIsAdmin(data.permissions?.includes("is_admin") || false);
}
setIsLoading(false);
}
Expand All @@ -215,6 +243,9 @@ const Profile = () => {
} else {
setUser(data);
setCanEdit(auth.currentUser?.id === data.id);
setIsAdmin(
auth.currentUser?.permissions?.includes("is_admin") || false,
);
}
setIsLoading(false);
} catch (err) {
Expand Down Expand Up @@ -296,6 +327,7 @@ const Profile = () => {
onUpdateProfile={handleUpdateProfile}
canEdit={canEdit}
listingIds={listingIds}
isAdmin={isAdmin}
/>
) : (
<div className="flex justify-center items-center pt-8">
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/ui/Input/PasswordInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const PasswordInput = <T extends FieldValues>({
)}
{showStrength && password.length > 0 && (
<>
<div className="mt-4 h-2 w-full bg-gray-12 rounded">
<div className="mt-4 h-2 w-full bg-gray-4 rounded">
<div
className={`h-full bg-${getStrengthColor(passwordStrength)} rounded`}
style={{ width: `${(passwordStrength + 1) * 20}%` }}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const buttonVariants = cva(
default: "bg-gray-12 text-gray-1 hover:bg-gray-12/80 hover:text-gray-1",
selected: "bg-primary-9 text-gray-1 hover:bg-primary-10",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
"bg-destructive text-gray-1 shadow-sm hover:bg-destructive/80",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
Expand Down
63 changes: 60 additions & 3 deletions frontend/src/gen/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/users/set-moderator": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Set Moderator */
post: operations["set_moderator_users_set_moderator_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
Expand Down Expand Up @@ -945,7 +962,7 @@ export interface components {
/** Google Id */
google_id: string | null;
/** Permissions */
permissions: "is_admin"[] | null;
permissions: ("is_admin" | "is_mod")[] | null;
/** First Name */
first_name: string | null;
/** Last Name */
Expand Down Expand Up @@ -990,7 +1007,7 @@ export interface components {
/** Email */
email: string;
/** Permissions */
permissions?: "is_admin"[] | null;
permissions?: ("is_admin" | "is_mod")[] | null;
/** Created At */
created_at?: number | null;
/** Updated At */
Expand All @@ -1009,6 +1026,13 @@ export interface components {
/** Users */
users: components["schemas"]["PublicUserInfoResponseItem"][];
};
/** SetModeratorRequest */
SetModeratorRequest: {
/** User Id */
user_id: string;
/** Is Mod */
is_mod: boolean;
};
/** SetRequest */
SetRequest: {
/** Onshape Url */
Expand Down Expand Up @@ -1097,7 +1121,7 @@ export interface components {
/** Email */
email: string;
/** Permissions */
permissions?: "is_admin"[] | null;
permissions?: ("is_admin" | "is_mod")[] | null;
/** Created At */
created_at: number;
/** Updated At */
Expand Down Expand Up @@ -2425,4 +2449,37 @@ export interface operations {
};
};
};
set_moderator_users_set_moderator_post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["SetModeratorRequest"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["UserPublic"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
}
2 changes: 1 addition & 1 deletion frontend/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export default {
foreground: "var(--gray12)",
},
destructive: {
DEFAULT: "#7F1D1D",
DEFAULT: "#DD4425",
foreground: "var(--gray1)",
},
border: "var(--gray6)",
Expand Down
11 changes: 11 additions & 0 deletions store/app/crud/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,17 @@ async def update_user(self, user_id: str, updates: dict[str, Any]) -> User:

return await self.get_user(user_id, throw_if_missing=True)

async def set_moderator(self, user_id: str, is_mod: bool) -> User:
user = await self.get_user(user_id, throw_if_missing=True)
if user.permissions is None:
user.permissions = set()
if is_mod:
user.permissions.add("is_mod")
else:
user.permissions.discard("is_mod")
await self._update_item(user_id, User, {"permissions": list(user.permissions)})
return user


async def test_adhoc() -> None:
async with UserCrud() as crud:
Expand Down
4 changes: 2 additions & 2 deletions store/app/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class StoreBaseModel(BaseModel):
id: str


UserPermission = Literal["is_admin"]
UserPermission = Literal["is_admin", "is_mod"]


class User(StoreBaseModel):
Expand Down Expand Up @@ -482,7 +482,7 @@ async def can_write_artifact(user: User, artifact: Artifact) -> bool:


async def can_write_listing(user: User, listing: Listing) -> bool:
if user.permissions is not None and "is_admin" in user.permissions:
if user.permissions is not None and ("is_admin" in user.permissions or "is_mod" in user.permissions):
return True
if user.id == listing.user_id:
return True
Expand Down
15 changes: 15 additions & 0 deletions store/app/routers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,3 +368,18 @@ async def validate_api_key_endpoint(

users_router.include_router(github_auth_router, prefix="/github")
users_router.include_router(google_auth_router, prefix="/google")


class SetModeratorRequest(BaseModel):
user_id: str
is_mod: bool


@users_router.post("/set-moderator")
async def set_moderator(
request: SetModeratorRequest,
admin_user: Annotated[User, Depends(get_session_user_with_admin_permission)],
crud: Annotated[Crud, Depends(Crud.get)],
) -> UserPublic:
updated_user = await crud.set_moderator(request.user_id, request.is_mod)
return UserPublic(**updated_user.model_dump())

0 comments on commit 3ab2565

Please sign in to comment.