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:
parent
fb059ae5d1
commit
69eb68a9a4
4 changed files with 111 additions and 14 deletions
|
|
@ -2,11 +2,25 @@
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<user-login
|
<user-login
|
||||||
class="is-justify-content-end mb-6"
|
class="is-justify-content-end"
|
||||||
@login="(login) => (user = login)"
|
@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">
|
<div class="control is-expanded">
|
||||||
<input
|
<input
|
||||||
class="input is-loading"
|
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)
|
const resp = await window.fetch(url, opts)
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw resp
|
||||||
|
}
|
||||||
return await resp.json()
|
return await resp.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,16 +161,25 @@ function debounce_async(ms, func) {
|
||||||
|
|
||||||
const debounced_get = debounce_async(100, get)
|
const debounced_get = debounce_async(100, get)
|
||||||
|
|
||||||
|
type ULID = string
|
||||||
|
type User = {
|
||||||
|
id: ULID,
|
||||||
|
name: string,
|
||||||
|
groups: Array<{id: ULID}>,
|
||||||
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
data: () => ({
|
data: () => ({
|
||||||
query: "",
|
query: "",
|
||||||
items: [],
|
items: [],
|
||||||
user: { user_id: "", secret: "" },
|
credentials: { user_id: "", secret: "" },
|
||||||
|
login_user: null,
|
||||||
page: 1,
|
page: 1,
|
||||||
is_end: false,
|
is_end: false,
|
||||||
media_types,
|
media_types,
|
||||||
media_type: "",
|
media_type: "",
|
||||||
active: false,
|
active: false,
|
||||||
|
filter_group: '',
|
||||||
}),
|
}),
|
||||||
mounted() {
|
mounted() {
|
||||||
const { query, type } = view_params()
|
const { query, type } = view_params()
|
||||||
|
|
@ -183,6 +209,8 @@ export default defineComponent({
|
||||||
|
|
||||||
const per_page = 100
|
const per_page = 100
|
||||||
const media_type = this.media_type === "" ? null : this.media_type
|
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(
|
const more = await debounced_get(
|
||||||
"movies",
|
"movies",
|
||||||
{
|
{
|
||||||
|
|
@ -191,9 +219,10 @@ export default defineComponent({
|
||||||
include_unrated: true,
|
include_unrated: true,
|
||||||
page: this.page,
|
page: this.page,
|
||||||
media_type,
|
media_type,
|
||||||
user_id: this.user.user_id,
|
user_id,
|
||||||
|
group_id,
|
||||||
},
|
},
|
||||||
this.user,
|
this.credentials,
|
||||||
)
|
)
|
||||||
this.items = this.items.concat(more)
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
@change="active = true"
|
@change="active = true"
|
||||||
/>
|
/>
|
||||||
<span class="icon is-small is-left">
|
<span class="icon is-small is-left">
|
||||||
<i class="fas fa-user"></i>
|
<span class="fas fa-user">👤</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="control has-icons-left">
|
<p class="control has-icons-left">
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
@change="active = true"
|
@change="active = true"
|
||||||
/>
|
/>
|
||||||
<span class="icon is-small is-left">
|
<span class="icon is-small is-left">
|
||||||
<i class="fas fa-lock"></i>
|
<span class="fas fa-lock">🔓</span>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
|
|
@ -36,12 +36,13 @@
|
||||||
import { defineComponent } from "vue"
|
import { defineComponent } from "vue"
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
emits: ["login"],
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
user_id: window.localStorage.user_id || "",
|
user_id: window.localStorage.user_id || "",
|
||||||
secret: window.localStorage.secret || "",
|
secret: window.localStorage.secret || "",
|
||||||
active: true,
|
active: true,
|
||||||
}),
|
}),
|
||||||
emits: ["login"],
|
|
||||||
mounted() {
|
mounted() {
|
||||||
const { user_id, secret } = this
|
const { user_id, secret } = this
|
||||||
if (user_id && secret) {
|
if (user_id && secret) {
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,13 @@ class Rating:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Access = Literal[
|
||||||
|
"r", # read
|
||||||
|
"i", # index
|
||||||
|
"w", # write
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class User:
|
class User:
|
||||||
_table: ClassVar[str] = "users"
|
_table: ClassVar[str] = "users"
|
||||||
|
|
@ -286,9 +293,18 @@ class User:
|
||||||
secret: str = None
|
secret: str = None
|
||||||
groups: list[dict[str, str]] = field(default_factory=list)
|
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)
|
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
|
@dataclass
|
||||||
|
|
|
||||||
|
|
@ -404,16 +404,32 @@ async def add_user(request):
|
||||||
|
|
||||||
|
|
||||||
@route("/users/{user_id}")
|
@route("/users/{user_id}")
|
||||||
@requires(["authenticated", "admin"])
|
@requires(["authenticated"])
|
||||||
async def show_user(request):
|
async def show_user(request):
|
||||||
|
|
||||||
user_id = as_ulid(request.path_params["user_id"])
|
user_id = as_ulid(request.path_params["user_id"])
|
||||||
|
|
||||||
|
if is_admin(request):
|
||||||
user = await db.get(User, id=str(user_id))
|
user = await db.get(User, id=str(user_id))
|
||||||
|
|
||||||
|
else:
|
||||||
|
user = await auth_user(request)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
return not_found()
|
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"])
|
@route("/users/{user_id}", methods=["DELETE"])
|
||||||
|
|
@ -482,6 +498,31 @@ async def modify_user(request):
|
||||||
return JSONResponse(asplain(user))
|
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")
|
@route("/users/{user_id}/ratings")
|
||||||
@requires(["private"])
|
@requires(["private"])
|
||||||
async def ratings_for_user(request):
|
async def ratings_for_user(request):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue