Add advanced sub permissions
This commit is contained in:
parent
05d5093e23
commit
40486f81fd
9 changed files with 405 additions and 172 deletions
|
|
@ -40,6 +40,10 @@ class User(BaseModel):
|
|||
verify_mastodon_favorite = BooleanField(null=False, default=False)
|
||||
verify_delay = IntegerField(null=True)
|
||||
|
||||
permission_orders_pools_view = BooleanField(null=False, default=True)
|
||||
permission_orders_pools_details = BooleanField(null=False, default=True)
|
||||
permission_orders_pools_edit = BooleanField(null=False, default=True)
|
||||
|
||||
def mastodon_account(self):
|
||||
if self.mastodon_server is None or self.mastodon_username is None:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -54,6 +54,42 @@ def user_preferences_set(id, mastodon_post_public, mastodon_attn_list):
|
|||
def user_has_doms(id):
|
||||
return DomSubUsers.select().where(DomSubUsers.sub_id == id).count() > 0
|
||||
|
||||
def user_doms(id):
|
||||
return [d.dom for d in DomSubUsers.select(DomSubUsers.dom).where(DomSubUsers.sub_id == id)]
|
||||
|
||||
def user_can_orders_pools_view(user, sub):
|
||||
doms = user_doms(sub.id)
|
||||
|
||||
if len(doms) > 0:
|
||||
if user == sub:
|
||||
return user.permission_orders_pools_view
|
||||
else:
|
||||
return user in doms
|
||||
else:
|
||||
return user.id == sub.id
|
||||
|
||||
def user_can_orders_pools_details(user, sub):
|
||||
doms = user_doms(sub.id)
|
||||
|
||||
if len(doms) > 0:
|
||||
if user == sub:
|
||||
return user.permission_orders_pools_details
|
||||
else:
|
||||
return user in doms
|
||||
else:
|
||||
return user.id == sub.id
|
||||
|
||||
def user_can_orders_pools_edit(user, sub):
|
||||
doms = user_doms(sub.id)
|
||||
|
||||
if len(doms) > 0:
|
||||
if user == sub:
|
||||
return user.permission_orders_pools_edit
|
||||
else:
|
||||
return user in doms
|
||||
else:
|
||||
return user.id == sub.id
|
||||
|
||||
def mastodon_server_get(name):
|
||||
return MastodonServer.get(name=name)
|
||||
|
||||
|
|
|
|||
51
migrations/029_add_sub_permissions.py
Normal file
51
migrations/029_add_sub_permissions.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
"""Peewee migrations -- 029_add_sub_permissions.py.
|
||||
|
||||
Some examples (model - class or model name)::
|
||||
|
||||
> Model = migrator.orm['table_name'] # Return model in current state by name
|
||||
> Model = migrator.ModelClass # Return model in current state by name
|
||||
|
||||
> migrator.sql(sql) # Run custom SQL
|
||||
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
||||
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
||||
> migrator.remove_model(model, cascade=True) # Remove a model
|
||||
> migrator.add_fields(model, **fields) # Add fields to a model
|
||||
> migrator.change_fields(model, **fields) # Change fields
|
||||
> migrator.remove_fields(model, *field_names, cascade=True)
|
||||
> migrator.rename_field(model, old_field_name, new_field_name)
|
||||
> migrator.rename_table(model, new_table_name)
|
||||
> migrator.add_index(model, *col_names, unique=False)
|
||||
> migrator.add_not_null(model, *field_names)
|
||||
> migrator.add_default(model, field_name, default)
|
||||
> migrator.add_constraint(model, name, sql)
|
||||
> migrator.drop_index(model, *col_names)
|
||||
> migrator.drop_not_null(model, *field_names)
|
||||
> migrator.drop_constraints(model, *constraints)
|
||||
|
||||
"""
|
||||
|
||||
from contextlib import suppress
|
||||
|
||||
import peewee as pw
|
||||
from peewee_migrate import Migrator
|
||||
|
||||
|
||||
with suppress(ImportError):
|
||||
import playhouse.postgres_ext as pw_pext
|
||||
|
||||
|
||||
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your migrations here."""
|
||||
|
||||
migrator.add_fields(
|
||||
'user',
|
||||
|
||||
permission_orders_pools_view=pw.BooleanField(default=True),
|
||||
permission_orders_pools_details=pw.BooleanField(default=True),
|
||||
permission_orders_pools_edit=pw.BooleanField(default=True))
|
||||
|
||||
|
||||
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
||||
"""Write your rollback migrations here."""
|
||||
|
||||
migrator.remove_fields('user', 'permission_orders_pools_view', 'permission_orders_pools_details', 'permission_orders_pools_edit')
|
||||
77
web/api.py
77
web/api.py
|
|
@ -6,7 +6,7 @@ from flask import Blueprint, jsonify, abort, request
|
|||
from flask_login import login_required, current_user
|
||||
from db.constants import TIMELINE_ORDERS_POOL_CREATED, TIMELINE_ORDERS_POOL_DELETED, TIMELINE_ORDERS_POOL_UPDATED
|
||||
from db.models import database, OrdersPool, Order, OrderAddOn, MastodonServer
|
||||
from db.queries import timeline_event_put, timeline_event_recent, user_get, domsubusers_list, orders_pool_list, orders_pool, mastodon_server_get, mastodon_server_put, user_has_doms, user_mastodon_server_set, user_preferences_set
|
||||
from db.queries import timeline_event_put, timeline_event_recent, user_can_orders_pools_details, user_can_orders_pools_edit, user_can_orders_pools_view, user_get, domsubusers_list, orders_pool_list, orders_pool, mastodon_server_get, mastodon_server_put, user_has_doms, user_mastodon_server_set, user_preferences_set
|
||||
from settings import MASTODON_OAUTH_CLIENT_NAME, MASTODON_OAUTH_REDIRECT_URI, MASTODON_OAUTH_SCOPES, MASTODON_OAUTH_CLIENT_WEBSITE
|
||||
from util import time_sqlite
|
||||
|
||||
|
|
@ -163,11 +163,11 @@ def authorized_sub(func):
|
|||
@login_required
|
||||
@authorized_sub
|
||||
def sub(username, sub):
|
||||
if request.method == "POST":
|
||||
if user_has_doms(sub) and sub.id == current_user.db_user.id:
|
||||
abort(403)
|
||||
return
|
||||
|
||||
if request.method == "POST":
|
||||
sub.verify_mastodon_favorite = bool(request.json['verify_mastodon_favorite'])
|
||||
sub.verify_mastodon_alt_text = bool(request.json['verify_mastodon_alt_text'])
|
||||
if request.json['verify_delay'] is not None:
|
||||
|
|
@ -189,7 +189,17 @@ def sub(username, sub):
|
|||
@api.route('/orders/')
|
||||
@login_required
|
||||
def my_order_sets():
|
||||
return jsonify([
|
||||
user = current_user.db_user
|
||||
result = {
|
||||
'permissions': {
|
||||
'can_view': user_can_orders_pools_view(user, user),
|
||||
'can_details': user_can_orders_pools_details(user, user),
|
||||
'can_edit': user_can_orders_pools_edit(user, user)
|
||||
}
|
||||
}
|
||||
|
||||
if result['permissions']['can_details']:
|
||||
result['pools'] = [
|
||||
{
|
||||
'id': op.id,
|
||||
'name': op.name,
|
||||
|
|
@ -207,13 +217,39 @@ def my_order_sets():
|
|||
}
|
||||
for op
|
||||
in orders_pool_list(current_user.db_user)
|
||||
])
|
||||
]
|
||||
elif result['permissions']['can_view']:
|
||||
result['pools'] = [
|
||||
{
|
||||
'id': op.id,
|
||||
'name': op.name,
|
||||
'scheduled': op.scheduled,
|
||||
'time': op.time,
|
||||
'weekends': op.weekends,
|
||||
'weekdays': op.weekdays,
|
||||
'probability': op.probability,
|
||||
'punishment_pool_name': op.punishment_pool.name if op.punishment_pool is not None else None,
|
||||
}
|
||||
for op
|
||||
in orders_pool_list(current_user.db_user)
|
||||
]
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@api.route('/orders/<username>/sets')
|
||||
@login_required
|
||||
@authorized_sub
|
||||
def sub_order_sets(username, sub):
|
||||
return jsonify([
|
||||
result = {
|
||||
'permissions': {
|
||||
'can_view': user_can_orders_pools_view(current_user.db_user, sub),
|
||||
'can_details': user_can_orders_pools_details(current_user.db_user, sub),
|
||||
'can_edit': user_can_orders_pools_edit(current_user.db_user, sub)
|
||||
}
|
||||
}
|
||||
|
||||
if result['permissions']['can_details']:
|
||||
result['pools'] = [
|
||||
{
|
||||
'id': op.id,
|
||||
'name': op.name,
|
||||
|
|
@ -231,12 +267,33 @@ def sub_order_sets(username, sub):
|
|||
}
|
||||
for op
|
||||
in orders_pool_list(sub.id)
|
||||
])
|
||||
]
|
||||
elif result['permissions']['can_view']:
|
||||
result['pools'] = [
|
||||
{
|
||||
'id': op.id,
|
||||
'name': op.name,
|
||||
'scheduled': op.scheduled,
|
||||
'time': op.time,
|
||||
'weekends': op.weekends,
|
||||
'weekdays': op.weekdays,
|
||||
'probability': op.probability,
|
||||
'punishment_pool_name': op.punishment_pool.name if op.punishment_pool is not None else None,
|
||||
}
|
||||
for op
|
||||
in orders_pool_list(sub.id)
|
||||
]
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@api.route('/orders/<username>/sets/', methods=['POST'])
|
||||
@login_required
|
||||
@authorized_sub
|
||||
def sub_order_set_create(username, sub):
|
||||
if not user_can_orders_pools_edit(current_user.db_user, sub):
|
||||
abort(403)
|
||||
return
|
||||
|
||||
# Create new
|
||||
with database.atomic() as transaction:
|
||||
try:
|
||||
|
|
@ -287,6 +344,10 @@ def sub_order_set(username, set_id, sub):
|
|||
op = orders_pool(sub.id, set_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
if not user_can_orders_pools_edit(current_user.db_user, sub):
|
||||
abort(403)
|
||||
return
|
||||
|
||||
def update_add_ons(order, add_ons):
|
||||
for updated_add_on in add_ons:
|
||||
if isinstance(updated_add_on['id'], int):
|
||||
|
|
@ -371,6 +432,10 @@ def sub_order_set(username, set_id, sub):
|
|||
abort(500)
|
||||
elif request.method == 'DELETE':
|
||||
try:
|
||||
if not user_can_orders_pools_edit(current_user.db_user, sub):
|
||||
abort(403)
|
||||
return
|
||||
|
||||
op.delete_instance(recursive=True)
|
||||
|
||||
timeline_event_put(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Text, Title, Flex, Card, Image } from "@mantine/core";
|
|||
import { useLoaderData } from "react-router";
|
||||
import { IconPencil } from "@tabler/icons-react";
|
||||
import { NavigateButton } from "./NavigateButton";
|
||||
import { OrderSetProps, OrderSets } from "./OrderSets";
|
||||
import { OrderSets, OrderSetsResponse } from "./OrderSets";
|
||||
import { useUserContext } from "./UserContext";
|
||||
import { TimelineList } from "./TimelineList";
|
||||
|
||||
|
|
@ -54,14 +54,20 @@ const SubsList: React.FC<SubsListProps> = ({ subs }) => (
|
|||
export const Dashboard: React.FC = () => {
|
||||
const [orderSets, subs, timeline] =
|
||||
useLoaderData<
|
||||
[OrderSetProps["orderSets"], SubsListProps["subs"], TimelineEvent[]]
|
||||
[OrderSetsResponse, SubsListProps["subs"], TimelineEvent[]]
|
||||
>();
|
||||
const { username } = useUserContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
{timeline.length > 0 ? <TimelineList timeline={timeline} /> : null}
|
||||
<OrderSets orderSets={orderSets} username={username} />
|
||||
{orderSets.permissions?.can_view && orderSets.pools.length > 0 ? (
|
||||
<OrderSets
|
||||
orderSets={orderSets.pools}
|
||||
permissions={orderSets.permissions}
|
||||
username={username}
|
||||
/>
|
||||
) : null}
|
||||
{subs.length > 0 ? <SubsList subs={subs} /> : null}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -83,9 +83,22 @@ export const OrderSet: React.FC = () => {
|
|||
|
||||
const [orderSets, setOrderSets] = React.useState([]);
|
||||
React.useEffect(() => {
|
||||
fetch(`/api/orders/${username}/sets`)
|
||||
.then((response) => response.json())
|
||||
.then(setOrderSets);
|
||||
fetch(`/api/orders/${username}/sets`).then(async (response) => {
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.permissions?.can_edit) {
|
||||
notifications.show({
|
||||
title: "Error",
|
||||
message: "Not authorized",
|
||||
color: "red",
|
||||
});
|
||||
navigate(`/dashboard/`);
|
||||
}
|
||||
|
||||
setOrderSets(data.pools);
|
||||
}
|
||||
});
|
||||
}, [username]);
|
||||
|
||||
const [showScheduling, setShowScheduling] = React.useState(
|
||||
|
|
|
|||
|
|
@ -32,6 +32,28 @@ const COLORS_ROTATION = [
|
|||
"gray.4",
|
||||
];
|
||||
|
||||
export interface OrderSetsResponse {
|
||||
pools: (Pick<
|
||||
OrderSet,
|
||||
| "id"
|
||||
| "name"
|
||||
| "scheduled"
|
||||
| "time"
|
||||
| "weekends"
|
||||
| "weekdays"
|
||||
| "orders"
|
||||
| "probability"
|
||||
> & {
|
||||
orders: Pick<OrderSetOrder, "id" | "name" | "weight">;
|
||||
punishment_pool_name: string;
|
||||
})[];
|
||||
permissions: {
|
||||
can_view: boolean;
|
||||
can_details: boolean;
|
||||
can_edit: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface OrderSetProps {
|
||||
orderSets: (Pick<
|
||||
OrderSet,
|
||||
|
|
@ -49,12 +71,14 @@ export interface OrderSetProps {
|
|||
})[];
|
||||
username: string;
|
||||
linkBack?: React.ReactNode;
|
||||
permissions?: OrderSetsResponse["permissions"];
|
||||
}
|
||||
|
||||
export const OrderSets: React.FC<OrderSetProps> = ({
|
||||
orderSets,
|
||||
username,
|
||||
linkBack,
|
||||
permissions,
|
||||
}) => {
|
||||
const { username: current_user } = useUserContext();
|
||||
const fetcher = useFetcher();
|
||||
|
|
@ -71,12 +95,13 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
|||
const [isMastodonSet, setIsMastodonSet] = React.useState(true);
|
||||
React.useEffect(() => {
|
||||
if (username) {
|
||||
fetch(`/api/subs/${username}`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
fetch(`/api/subs/${username}`).then(async (response) => {
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (!data.mastodon_server || !data.mastodon_username) {
|
||||
setIsMastodonSet(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [username]);
|
||||
|
|
@ -84,12 +109,12 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
|||
const [portalRef, setPortalRef] = React.useState<HTMLElement | null>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box my="lg">
|
||||
<Box mb="lg">
|
||||
<Title order={1}>Order Sets for {username}</Title>
|
||||
{linkBack ? linkBack : null}
|
||||
</Box>
|
||||
{orderSets.length > 0 && isMastodonSet ? null : (
|
||||
{isMastodonSet ? null : (
|
||||
<Flex justify="center">
|
||||
<Alert
|
||||
variant="light"
|
||||
|
|
@ -119,6 +144,7 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
|||
}
|
||||
}}
|
||||
>
|
||||
{permissions?.can_view ? (
|
||||
<Grid gutter="md">
|
||||
{orderSets
|
||||
? orderSets.map(
|
||||
|
|
@ -175,7 +201,10 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
|||
</Text>
|
||||
}
|
||||
sections={[
|
||||
{ color: "orange", value: probability * 100 },
|
||||
{
|
||||
color: "orange",
|
||||
value: probability * 100,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Flex>
|
||||
|
|
@ -187,7 +216,7 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
|||
</Text>
|
||||
) : null}
|
||||
<Flex justify="end" align="flex-end" gap="sm">
|
||||
{orders.length > 0 ? (
|
||||
{permissions?.can_details && orders.length > 0 ? (
|
||||
<DonutChart
|
||||
flex={1}
|
||||
size={130}
|
||||
|
|
@ -196,11 +225,15 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
|||
name,
|
||||
value: weight,
|
||||
color:
|
||||
COLORS_ROTATION[idx % COLORS_ROTATION.length],
|
||||
COLORS_ROTATION[
|
||||
idx % COLORS_ROTATION.length
|
||||
],
|
||||
}))}
|
||||
tooltipDataSource="segment"
|
||||
/>
|
||||
) : null}
|
||||
{permissions?.can_edit ? (
|
||||
<>
|
||||
<ConfirmDialogButton
|
||||
buttonColor="red.8"
|
||||
buttonText="Delete"
|
||||
|
|
@ -210,10 +243,16 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
|||
>
|
||||
<IconTrash />
|
||||
</ConfirmDialogButton>
|
||||
<NavigateButton to={`/orders/${username}/${id}`}>
|
||||
<IconPencil style={{ marginRight: "0.5rem" }} />
|
||||
<NavigateButton
|
||||
to={`/orders/${username}/${id}`}
|
||||
>
|
||||
<IconPencil
|
||||
style={{ marginRight: "0.5rem" }}
|
||||
/>
|
||||
Edit
|
||||
</NavigateButton>
|
||||
</>
|
||||
) : null}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
|
@ -222,13 +261,29 @@ export const OrderSets: React.FC<OrderSetProps> = ({
|
|||
)
|
||||
: null}
|
||||
</Grid>
|
||||
) : (
|
||||
<Flex justify="center">
|
||||
<Alert
|
||||
variant="light"
|
||||
color="orange"
|
||||
title="Warning"
|
||||
icon={<IconAlertTriangle />}
|
||||
my="md"
|
||||
w="40rem"
|
||||
>
|
||||
You are not authorized to view order pools for <b>{username}</b>.
|
||||
</Alert>
|
||||
</Flex>
|
||||
)}
|
||||
</div>
|
||||
{permissions?.can_edit ? (
|
||||
<Box my="lg">
|
||||
<NavigateButton to={`/orders/${username}/new`}>
|
||||
<IconPlus style={{ marginRight: "0.5rem" }} />
|
||||
New
|
||||
</NavigateButton>
|
||||
</Box>
|
||||
</>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,31 +1,34 @@
|
|||
import React from "react";
|
||||
import { Params, useLoaderData, useParams, Link } from "react-router";
|
||||
import { OrderSetProps, OrderSets } from "./OrderSets";
|
||||
import { OrderSetProps, OrderSets, OrderSetsResponse } from "./OrderSets";
|
||||
import { ProfileVerification } from "./ProfileVerification";
|
||||
import { Anchor, Title } from "@mantine/core";
|
||||
|
||||
export const subOrderSetsLoader = async ({
|
||||
export const userOrderSetsLoader = async ({
|
||||
params: { username },
|
||||
}: {
|
||||
params: Params<string>;
|
||||
}) => fetch(`/api/orders/${username}/sets`).then((response) => response.json());
|
||||
|
||||
export const SubOrderSets: React.FC = () => {
|
||||
export const UserOrderSets: React.FC = () => {
|
||||
const { username: sub_username } = useParams();
|
||||
const orderSets = useLoaderData<OrderSetProps["orderSets"]>();
|
||||
const orderSets = useLoaderData<OrderSetsResponse>();
|
||||
|
||||
const [profile, setProfile] = React.useState<UserProfile | null>(null);
|
||||
React.useEffect(() => {
|
||||
fetch(`/api/subs/${sub_username}`)
|
||||
.then((response) => response.json())
|
||||
.then(setProfile);
|
||||
// fetch(`/api/subs/${sub_username}`).then(async (response) => {
|
||||
// if (response.ok) {
|
||||
// setProfile(await response.json());
|
||||
// }
|
||||
// });
|
||||
}, [sub_username]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OrderSets
|
||||
username={sub_username}
|
||||
orderSets={orderSets}
|
||||
orderSets={orderSets.pools}
|
||||
permissions={orderSets.permissions}
|
||||
linkBack={
|
||||
<Anchor component={Link} to={`/dashboard/`}>
|
||||
Return to dashboard
|
||||
|
|
@ -21,7 +21,7 @@ import "@mantine/notifications/styles.css";
|
|||
import "@mantine/charts/styles.css";
|
||||
|
||||
import { Dashboard, subsListLoader } from "./Dashboard";
|
||||
import { SubOrderSets, subOrderSetsLoader } from "./SubOrderSets";
|
||||
import { UserOrderSets, userOrderSetsLoader } from "./UserOrderSets";
|
||||
import { OrderSet, orderSetLoader, orderSetAction } from "./OrderSet";
|
||||
import { UserContextProvider } from "./UserContext";
|
||||
import { Header } from "./Header";
|
||||
|
|
@ -75,8 +75,8 @@ const router = createBrowserRouter([
|
|||
},
|
||||
{
|
||||
path: "orders/:username",
|
||||
Component: SubOrderSets,
|
||||
loader: subOrderSetsLoader,
|
||||
Component: UserOrderSets,
|
||||
loader: userOrderSetsLoader,
|
||||
},
|
||||
{
|
||||
path: "orders/:username/new",
|
||||
|
|
|
|||
Loading…
Reference in a new issue