add UI selector for user ratings (group or own)

Allow a user to access their own dataset.
Add a route for admins to give users access to groups.
This commit is contained in:
ducklet 2021-08-05 15:53:27 +02:00
parent fb059ae5d1
commit 69eb68a9a4
4 changed files with 111 additions and 14 deletions

View file

@ -2,11 +2,25 @@
<section class="section">
<div class="container">
<user-login
class="is-justify-content-end mb-6"
@login="(login) => (user = login)"
class="is-justify-content-end"
@login="(login) => (credentials = login)"
/>
<div class="field has-addons">
<div class="field is-flex is-justify-content-end" v-if="login_user">
<div class="control has-icons-left">
<div class="select is-small">
<select v-model="filter_group" @change="search">
<option selected value="">Me ({{ login_user.name }})</option>
<option v-for="g in login_user.groups" :value="g.id">{{g.id}}</option>
</select>
</div>
<div class="icon is-small is-left">
<span class="fas fa-group">👥</span>
</div>
</div>
</div>
<div class="field has-addons mt-6">
<div class="control is-expanded">
<input
class="input is-loading"
@ -105,6 +119,9 @@ async function req(url, opts = {}, { user_id = "", secret = "" }, data = null) {
}
const resp = await window.fetch(url, opts)
if (!resp.ok) {
throw resp
}
return await resp.json()
}
@ -144,16 +161,25 @@ function debounce_async(ms, func) {
const debounced_get = debounce_async(100, get)
type ULID = string
type User = {
id: ULID,
name: string,
groups: Array<{id: ULID}>,
}
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
},
},
})
</script>

View file

@ -9,7 +9,7 @@
@change="active = true"
/>
<span class="icon is-small is-left">
<i class="fas fa-user"></i>
<span class="fas fa-user">👤</span>
</span>
</p>
<p class="control has-icons-left">
@ -21,7 +21,7 @@
@change="active = true"
/>
<span class="icon is-small is-left">
<i class="fas fa-lock"></i>
<span class="fas fa-lock">🔓</span>
</span>
</p>
<p class="control">
@ -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) {

View file

@ -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

View file

@ -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"])
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):