diff --git a/unwind-ui/src/App.vue b/unwind-ui/src/App.vue index f611e39..fd9c712 100644 --- a/unwind-ui/src/App.vue +++ b/unwind-ui/src/App.vue @@ -2,11 +2,25 @@
-
+
+
+
+ +
+
+ 👥 +
+
+
+ +
, +} + export default defineComponent({ data: () => ({ query: "", items: [], - user: { user_id: "", secret: "" }, + credentials: { user_id: "", secret: "" }, + login_user: null, page: 1, is_end: false, media_types, media_type: "", active: false, + filter_group: '', }), mounted() { const { query, type } = view_params() @@ -183,6 +209,8 @@ export default defineComponent({ const per_page = 100 const media_type = this.media_type === "" ? null : this.media_type + const user_id = this.filter_group === '' && this.credentials.user_id || null + const group_id = user_id === null && this.filter_group ? this.filter_group : null const more = await debounced_get( "movies", { @@ -191,9 +219,10 @@ export default defineComponent({ include_unrated: true, page: this.page, media_type, - user_id: this.user.user_id, + user_id, + group_id, }, - this.user, + this.credentials, ) this.items = this.items.concat(more) @@ -204,6 +233,16 @@ export default defineComponent({ } }, }, + watch: { + async credentials(creds) { + if (!creds.user_id) { + return + } + + const user: User = await get(`users/${creds.user_id}`, {}, creds) + this.login_user = user + }, + }, }) diff --git a/unwind-ui/src/components/UserLogin.vue b/unwind-ui/src/components/UserLogin.vue index d897a3f..578f978 100644 --- a/unwind-ui/src/components/UserLogin.vue +++ b/unwind-ui/src/components/UserLogin.vue @@ -9,7 +9,7 @@ @change="active = true" /> - + 👤

@@ -21,7 +21,7 @@ @change="active = true" /> - + 🔓

@@ -36,12 +36,13 @@ import { defineComponent } from "vue" export default defineComponent({ + emits: ["login"], + data: () => ({ user_id: window.localStorage.user_id || "", secret: window.localStorage.secret || "", active: true, }), - emits: ["login"], mounted() { const { user_id, secret } = this if (user_id && secret) { diff --git a/unwind/models.py b/unwind/models.py index 185b6d5..5944363 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -276,6 +276,13 @@ class Rating: ) +Access = Literal[ + "r", # read + "i", # index + "w", # write +] + + @dataclass class User: _table: ClassVar[str] = "users" @@ -286,9 +293,18 @@ class User: secret: str = None groups: list[dict[str, str]] = field(default_factory=list) - def has_access(self, group_id: Union[ULID, str], access: Literal["r", "w"] = "r"): + def has_access(self, group_id: Union[ULID, str], access: Access = "r"): group_id = group_id if isinstance(group_id, str) else str(group_id) - return any(g["id"] == group_id and access in g["access"] for g in self.groups) + return any(g["id"] == group_id and access == g["access"] for g in self.groups) + + def set_access(self, group_id: Union[ULID, str], access: Access): + group_id = group_id if isinstance(group_id, str) else str(group_id) + for g in self.groups: + if g["id"] == group_id: + g["access"] = access + break + else: + self.groups.append({"id": group_id, "access": access}) @dataclass diff --git a/unwind/web.py b/unwind/web.py index f043f18..a3376ab 100644 --- a/unwind/web.py +++ b/unwind/web.py @@ -404,16 +404,32 @@ async def add_user(request): @route("/users/{user_id}") -@requires(["authenticated", "admin"]) +@requires(["authenticated"]) async def show_user(request): user_id = as_ulid(request.path_params["user_id"]) - user = await db.get(User, id=str(user_id)) + if is_admin(request): + user = await db.get(User, id=str(user_id)) + + else: + user = await auth_user(request) + if not user: return not_found() - return JSONResponse(asplain(user)) + is_allowed = user.id == user_id + if not is_allowed: + return forbidden() + + # Redact `secret` + resp = asplain(user) + resp["secret"] = None + + # Fix `groups` + resp["groups"] = user.groups + + return JSONResponse(resp) @route("/users/{user_id}", methods=["DELETE"]) @@ -482,6 +498,31 @@ async def modify_user(request): return JSONResponse(asplain(user)) +@route("/users/{user_id}/groups", methods=["POST"]) +@requires(["authenticated", "admin"]) +async def add_group_to_user(request): + + user_id = as_ulid(request.path_params["user_id"]) + + user = await db.get(User, id=str(user_id)) + if not user: + return not_found("User not found") + + (group_id, access) = await json_from_body(request, ["group", "access"]) + + group = await db.get(Group, id=str(group_id)) + if not group: + return not_found("Group not found") + + if access not in set("riw"): + raise HTTPException(422, f"Invalid access level.") + + user.set_access(group_id, access) + await db.update(user) + + return JSONResponse(asplain(user)) + + @route("/users/{user_id}/ratings") @requires(["private"]) async def ratings_for_user(request):