feature: avatar uploads

This commit is contained in:
Andy Burke 2025-08-12 16:00:36 -07:00
parent ffe0678e5b
commit 01768da647
7 changed files with 139 additions and 18 deletions

View file

@ -18,6 +18,7 @@ export function load() {
const is_to_home_dir = meta.user?.id && path.toLowerCase().startsWith(`/files/users/${meta.user.id}/`);
const has_permission = is_to_files && (can_write_all_files || (can_write_own_files && is_to_home_dir));
if (!has_permission) {
return CANNED_RESPONSES.permission_denied();
}

View file

@ -24,6 +24,8 @@ const api = {
if (options.json) {
headers["Content-Type"] = "application/json";
fetch_options.body = JSON.stringify(options.json);
} else if (options.body) {
fetch_options.body = options.body;
}
const response = await fetch(url, fetch_options);

View file

@ -1,4 +1,6 @@
<script>
const DEFAULT_AVATAR_URL = `//${window.location.host}/images/default_avatar.gif`;
new MutationObserver((mutations, observer) => {
mutations.forEach((mutation) => {
const user_json = document.body.dataset.user;
@ -7,19 +9,48 @@
: {
username: "",
meta: {
avatar: "/images/default_avatar.gif",
avatar: DEFAULT_AVATAR_URL,
},
};
const avatars = document.querySelectorAll("[data-bind-user_meta_avatar]");
for (const avatar of avatars) {
const bound_to = avatar.dataset["bind-user_meta_avatar"] ?? "innerHTML";
avatar[bound_to] = user.meta?.avatar ?? "/images/default_avatar.gif";
console.dir({
user_json,
user,
});
const ids = document.querySelectorAll("[data-bind__user_id]");
for (const id of ids) {
const bound_to =
typeof id.dataset["bind__user_id"] === "string" &&
id.dataset["bind__user_id"].length > 0
? id.dataset["bind__user_id"]
: "innerHTML";
avatar[bound_to] = user.id ?? "<unknown>";
}
const usernames = document.querySelectorAll("[data-bind-user_username]");
const avatars = document.querySelectorAll("[data-bind__user_meta_avatar]");
for (const avatar of avatars) {
console.dir({
avatar,
});
const bound_to =
typeof avatar.dataset["bind__user_meta_avatar"] === "string" &&
avatar.dataset["bind__user_meta_avatar"].length
? avatar.dataset["bind__user_meta_avatar"]
: "innerHTML";
avatar[bound_to] = user.meta?.avatar ?? DEFAULT_AVATAR_URL;
console.dir({
avatar,
});
}
const usernames = document.querySelectorAll("[data-bind__user_username]");
for (const username of usernames) {
const bound_to = username.dataset["bind-user_username"] ?? "innerHTML";
const bound_to =
typeof username.dataset["bind__user_username"] === "string" &&
username.dataset["bind__user_username"].length > 0
? username.dataset["bind__user_username"]
: "innerHTML";
username[bound_to] = user.username;
}
});
@ -32,7 +63,33 @@
.profile-container {
margin: 1rem auto;
max-width: 1024px;
padding: 1rem;
}
.profile-container .avatar-container {
position: relative;
width: 100%;
max-width: 200px;
}
.profile-container .avatar-container #user-avatar {
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
.profile-container .avatar-container input[type="file"] {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0;
cursor: pointer;
}
.profile-container .avatar-container
</style>
<div id="user" class="tab">
<input
@ -53,12 +110,68 @@
id="user-avatar"
src="/images/default_avatar.gif"
alt="User Avatar"
data-bind-user_meta_avatar="src"
data-bind__user_meta_avatar="src"
/>
<input type="file" accept="image/*" name="avatar" />
<script>
const avatar_file_input = document.querySelector('input[name="avatar"]');
avatar_file_input.addEventListener("change", async (event) => {
const user_json = document.body.dataset.user;
if (!user_json) {
return alert("You must be logged in.");
}
const user = JSON.parse(user_json);
const avatar = avatar_file_input.files[0];
if (!avatar || !avatar.type || !avatar.type.includes("image")) {
return alert("You must select a valid image to upload as your avatar.");
}
// TODO: actually enforce this on the upload in serverus somehow
if (avatar.size > 512_000) {
return alert("512K is the largest allowed avatar size.");
}
const body = new FormData();
body.append("file", avatar, encodeURIComponent(avatar.name));
const avatar_path = `/files/users/${user.id}/avatars/${avatar.name}`;
const avatar_upload_response = await api.fetch(avatar_path, {
method: "PUT",
body,
});
if (!avatar_upload_response.ok) {
const error = await avatar_upload_response.json();
return alert(error?.error?.message ?? "Unknown error.");
}
const updated_user = { ...user };
updated_user.meta = updated_user.meta ?? {};
updated_user.meta.avatar = `//${window.location.host}${avatar_path}`;
const saved_user_response = await api.fetch(`/api/users/${user.id}`, {
method: "PUT",
json: updated_user,
});
if (!saved_user_response.ok) {
const error = await avatar_upload_response.json();
return alert(error?.error?.message ?? "Unknown error.");
}
const saved_user = await saved_user_response.json();
document.body.dataset.user = JSON.stringify(saved_user);
document.body.dataset.perms = saved_user.permissions.join(":");
});
</script>
</div>
<div class="username-container">
<span class="username" data-bind-user_username></span>
<span class="username" data-bind__user_username></span>
</div>
<form data-smart="true" data-method="DELETE" action="/api/auth">