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">
|
||||
<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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue