26
server/assets/pages/change_avatar.tmpl
Normal file
26
server/assets/pages/change_avatar.tmpl
Normal file
@@ -0,0 +1,26 @@
|
||||
{{ define "user" }}
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account-key"></i> changing {{ .SelectedUser.Name }}'s avatar
|
||||
</div>
|
||||
{{ if ne (len .SelectedUser.Avatar) 0 }}
|
||||
<form class="block" action="{{ printf "/admin/delete_avatar_do?user=%s" .SelectedUser.Name | path }}" method="post">
|
||||
<input type="submit" value="delete avatar">
|
||||
</form>
|
||||
{{ end }}
|
||||
<form
|
||||
class="block file-upload"
|
||||
enctype="multipart/form-data"
|
||||
action="{{ printf "/admin/change_avatar_do?user=%s" .SelectedUser.Name | path }}"
|
||||
method="post"
|
||||
>
|
||||
<div style="position: relative;">
|
||||
<input style="position: absolute; opacity: 0;" name="avatar" type="file" accept="image/jpeg image/png image/gif" />
|
||||
<input type="button" value="upload avatar">
|
||||
</div>
|
||||
{{ if ne (len .SelectedUser.Avatar) 0 }}
|
||||
<p><img class="avatar-preview" src="data:image/jpg;base64,{{ .SelectedUser.Avatar | base64 }}"/></p>
|
||||
{{ end }}
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
26
server/assets/pages/change_own_avatar.tmpl
Normal file
26
server/assets/pages/change_own_avatar.tmpl
Normal file
@@ -0,0 +1,26 @@
|
||||
{{ define "user" }}
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account-key"></i> changing account avatar
|
||||
</div>
|
||||
{{ if ne (len .SelectedUser.Avatar) 0 }}
|
||||
<form class="block" action="{{ path "/admin/delete_own_avatar_do" }}" method="post">
|
||||
<input type="submit" value="delete avatar">
|
||||
</form>
|
||||
{{ end }}
|
||||
<form
|
||||
class="block file-upload"
|
||||
enctype="multipart/form-data"
|
||||
action="{{ path "/admin/change_own_avatar_do" }}"
|
||||
method="post"
|
||||
>
|
||||
<div style="position: relative;">
|
||||
<input style="position: absolute; opacity: 0;" name="avatar" type="file" accept="image/jpeg image/png image/gif" />
|
||||
<input type="button" value="upload avatar">
|
||||
</div>
|
||||
{{ if ne (len .SelectedUser.Avatar) 0 }}
|
||||
<p><img class="avatar-preview" src="data:image/jpg;base64,{{ .SelectedUser.Avatar | base64 }}"/></p>
|
||||
{{ end }}
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -82,6 +82,8 @@
|
||||
<span class="text-light">|</span>
|
||||
<a href="{{ printf "/admin/change_password?user=%s" $user.Name | path }}">password…</a>
|
||||
<span class="text-light">|</span>
|
||||
<a href="{{ printf "/admin/change_avatar?user=%s" $user.Name | path }}">change avatar…</a>
|
||||
<span class="text-light">|</span>
|
||||
{{ if $user.IsAdmin }}
|
||||
<span class="text-light">delete…</span>
|
||||
{{ else }}
|
||||
@@ -100,6 +102,8 @@
|
||||
<a href="{{ path "/admin/change_own_username" }}" class="button">change username…</a>
|
||||
<span class="text-light">|</span>
|
||||
<a href="{{ path "/admin/change_own_password" }}" class="button">change password…</a>
|
||||
<span class="text-light">|</span>
|
||||
<a href="{{ path "/admin/change_own_avatar" }}" class="button">change avatar…</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -260,17 +264,16 @@
|
||||
{{ end }}
|
||||
</table>
|
||||
<form
|
||||
id="playlist-upload-form"
|
||||
class="file-upload"
|
||||
enctype="multipart/form-data"
|
||||
action="{{ path "/admin/upload_playlist_do" }}"
|
||||
method="post"
|
||||
>
|
||||
<div style="position: relative;">
|
||||
<input id="playlist-upload-input" style="position: absolute; opacity: 0;" name="playlist-files" type="file" multiple />
|
||||
<input style="position: absolute; opacity: 0;" name="playlist-files" type="file" multiple />
|
||||
<input type="button" value="upload m3u8">
|
||||
</div>
|
||||
</form>
|
||||
<script src="{{ path "/admin/static/playlist-upload.js" }}"></script>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
<link rel="shortcut icon" href="{{ path "/admin/static/favicon.ico" }}" type="image/x-icon">
|
||||
<link rel="icon" href="{{ path "/admin/static/favicon.ico" }}" type="image/x-icon">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<script src="{{ path "/admin/static/main.js" }}" defer></script>
|
||||
{{ end }}
|
||||
|
||||
@@ -178,3 +178,9 @@ a:hover {
|
||||
.angry {
|
||||
background-color: #f4433669;
|
||||
}
|
||||
|
||||
.avatar-preview {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
5
server/assets/static/main.js
Normal file
5
server/assets/static/main.js
Normal file
@@ -0,0 +1,5 @@
|
||||
for (const form of document.querySelectorAll("form.file-upload") || []) {
|
||||
const input = form.querySelector("input[type=file]");
|
||||
if (!input) continue;
|
||||
input.onchange = (e) => form.submit();
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
document.getElementById("playlist-upload-input").onchange = e => {
|
||||
document.getElementById("playlist-upload-form").submit();
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
package ctrladmin
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -21,10 +22,10 @@ import (
|
||||
"github.com/sentriz/gormstore"
|
||||
|
||||
"go.senan.xyz/gonic"
|
||||
"go.senan.xyz/gonic/server/assets"
|
||||
"go.senan.xyz/gonic/server/ctrlbase"
|
||||
"go.senan.xyz/gonic/db"
|
||||
"go.senan.xyz/gonic/podcasts"
|
||||
"go.senan.xyz/gonic/server/assets"
|
||||
"go.senan.xyz/gonic/server/ctrlbase"
|
||||
)
|
||||
|
||||
type CtxKey int
|
||||
@@ -47,6 +48,7 @@ func funcMap() template.FuncMap {
|
||||
return strings.ToLower(in.Format("Jan 02, 2006"))
|
||||
},
|
||||
"dateHuman": humanize.Time,
|
||||
"base64": base64.StdEncoding.EncodeToString,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,8 +124,11 @@ type templateData struct {
|
||||
DefaultListenBrainzURL string
|
||||
SelectedUser *db.User
|
||||
|
||||
Podcasts []*db.Podcast
|
||||
Podcasts []*db.Podcast
|
||||
InternetRadioStations []*db.InternetRadioStation
|
||||
|
||||
// avatar
|
||||
Avatar []byte
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package ctrladmin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif" // to decode uploaded GIF avatars
|
||||
"image/jpeg"
|
||||
_ "image/png" // to decode uploaded PNG avatars
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -9,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/mmcdole/gofeed"
|
||||
"github.com/nfnt/resize"
|
||||
|
||||
"go.senan.xyz/gonic/db"
|
||||
"go.senan.xyz/gonic/scanner"
|
||||
@@ -120,6 +126,37 @@ func (c *Controller) ServeChangeOwnPasswordDo(r *http.Request) *Response {
|
||||
return &Response{redirect: "/admin/home"}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangeOwnAvatar(r *http.Request) *Response {
|
||||
data := &templateData{}
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
data.SelectedUser = user
|
||||
return &Response{
|
||||
template: "change_own_avatar.tmpl",
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangeOwnAvatarDo(r *http.Request) *Response {
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
avatar, err := getAvatarFile(r)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
redirect: r.Referer(),
|
||||
flashW: []string{err.Error()},
|
||||
}
|
||||
}
|
||||
user.Avatar = avatar
|
||||
c.DB.Save(user)
|
||||
return &Response{redirect: "/admin/home"}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeDeleteOwnAvatarDo(r *http.Request) *Response {
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
user.Avatar = nil
|
||||
c.DB.Save(user)
|
||||
return &Response{redirect: "/admin/home"}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeLinkLastFMDo(r *http.Request) *Response {
|
||||
token := r.URL.Query().Get("token")
|
||||
if token == "" {
|
||||
@@ -242,6 +279,46 @@ func (c *Controller) ServeChangePasswordDo(r *http.Request) *Response {
|
||||
return &Response{redirect: "/admin/home"}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangeAvatar(r *http.Request) *Response {
|
||||
username := r.URL.Query().Get("user")
|
||||
if username == "" {
|
||||
return &Response{code: 400, err: "please provide a username"}
|
||||
}
|
||||
user := c.DB.GetUserByName(username)
|
||||
if user == nil {
|
||||
return &Response{code: 400, err: "couldn't find a user with that name"}
|
||||
}
|
||||
data := &templateData{}
|
||||
data.SelectedUser = user
|
||||
return &Response{
|
||||
template: "change_avatar.tmpl",
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangeAvatarDo(r *http.Request) *Response {
|
||||
username := r.URL.Query().Get("user")
|
||||
user := c.DB.GetUserByName(username)
|
||||
avatar, err := getAvatarFile(r)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
redirect: r.Referer(),
|
||||
flashW: []string{err.Error()},
|
||||
}
|
||||
}
|
||||
user.Avatar = avatar
|
||||
c.DB.Save(user)
|
||||
return &Response{redirect: "/admin/home"}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeDeleteAvatarDo(r *http.Request) *Response {
|
||||
username := r.URL.Query().Get("user")
|
||||
user := c.DB.GetUserByName(username)
|
||||
user.Avatar = nil
|
||||
c.DB.Save(user)
|
||||
return &Response{redirect: "/admin/home"}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeDeleteUser(r *http.Request) *Response {
|
||||
username := r.URL.Query().Get("user")
|
||||
if username == "" {
|
||||
@@ -278,8 +355,7 @@ func (c *Controller) ServeCreateUser(r *http.Request) *Response {
|
||||
|
||||
func (c *Controller) ServeCreateUserDo(r *http.Request) *Response {
|
||||
username := r.FormValue("username")
|
||||
err := validateUsername(username)
|
||||
if err != nil {
|
||||
if err := validateUsername(username); err != nil {
|
||||
return &Response{
|
||||
redirect: r.Referer(),
|
||||
flashW: []string{err.Error()},
|
||||
@@ -287,8 +363,7 @@ func (c *Controller) ServeCreateUserDo(r *http.Request) *Response {
|
||||
}
|
||||
passwordOne := r.FormValue("password_one")
|
||||
passwordTwo := r.FormValue("password_two")
|
||||
err = validatePasswords(passwordOne, passwordTwo)
|
||||
if err != nil {
|
||||
if err := validatePasswords(passwordOne, passwordTwo); err != nil {
|
||||
return &Response{
|
||||
redirect: r.Referer(),
|
||||
flashW: []string{err.Error()},
|
||||
@@ -570,3 +645,24 @@ func (c *Controller) ServeInternetRadioStationDeleteDo(r *http.Request) *Respons
|
||||
redirect: "/admin/home",
|
||||
}
|
||||
}
|
||||
|
||||
func getAvatarFile(r *http.Request) ([]byte, error) {
|
||||
err := r.ParseMultipartForm(10 << 20) // keep up to 10MB in memory
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, _, err := r.FormFile("avatar")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read form file: %w", err)
|
||||
}
|
||||
i, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode image: %w", err)
|
||||
}
|
||||
resized := resize.Resize(64, 64, i, resize.Lanczos3)
|
||||
var buff bytes.Buffer
|
||||
if err := jpeg.Encode(&buff, resized, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buff.Bytes(), nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ctrlsubsonic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -297,3 +298,18 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) ServeGetAvatar(w http.ResponseWriter, r *http.Request) *spec.Response {
|
||||
params := r.Context().Value(CtxParams).(params.Params)
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
username, err := params.Get("username")
|
||||
if err != nil {
|
||||
return spec.NewError(10, "please provide an `username` parameter")
|
||||
}
|
||||
reqUser := c.DB.GetUserByName(username)
|
||||
if (user != reqUser) && !user.IsAdmin {
|
||||
return spec.NewError(50, "user not admin")
|
||||
}
|
||||
http.ServeContent(w, r, "", time.Now(), bytes.NewReader(reqUser.Avatar))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -161,6 +161,9 @@ func setupAdmin(r *mux.Router, ctrl *ctrladmin.Controller) {
|
||||
routUser.Handle("/change_own_username_do", ctrl.H(ctrl.ServeChangeOwnUsernameDo))
|
||||
routUser.Handle("/change_own_password", ctrl.H(ctrl.ServeChangeOwnPassword))
|
||||
routUser.Handle("/change_own_password_do", ctrl.H(ctrl.ServeChangeOwnPasswordDo))
|
||||
routUser.Handle("/change_own_avatar", ctrl.H(ctrl.ServeChangeOwnAvatar))
|
||||
routUser.Handle("/change_own_avatar_do", ctrl.H(ctrl.ServeChangeOwnAvatarDo))
|
||||
routUser.Handle("/delete_own_avatar_do", ctrl.H(ctrl.ServeDeleteOwnAvatarDo))
|
||||
routUser.Handle("/link_lastfm_do", ctrl.H(ctrl.ServeLinkLastFMDo))
|
||||
routUser.Handle("/unlink_lastfm_do", ctrl.H(ctrl.ServeUnlinkLastFMDo))
|
||||
routUser.Handle("/link_listenbrainz_do", ctrl.H(ctrl.ServeLinkListenBrainzDo))
|
||||
@@ -177,6 +180,9 @@ func setupAdmin(r *mux.Router, ctrl *ctrladmin.Controller) {
|
||||
routAdmin.Handle("/change_username_do", ctrl.H(ctrl.ServeChangeUsernameDo))
|
||||
routAdmin.Handle("/change_password", ctrl.H(ctrl.ServeChangePassword))
|
||||
routAdmin.Handle("/change_password_do", ctrl.H(ctrl.ServeChangePasswordDo))
|
||||
routAdmin.Handle("/change_avatar", ctrl.H(ctrl.ServeChangeAvatar))
|
||||
routAdmin.Handle("/change_avatar_do", ctrl.H(ctrl.ServeChangeAvatarDo))
|
||||
routAdmin.Handle("/delete_avatar_do", ctrl.H(ctrl.ServeDeleteAvatarDo))
|
||||
routAdmin.Handle("/delete_user", ctrl.H(ctrl.ServeDeleteUser))
|
||||
routAdmin.Handle("/delete_user_do", ctrl.H(ctrl.ServeDeleteUserDo))
|
||||
routAdmin.Handle("/create_user", ctrl.H(ctrl.ServeCreateUser))
|
||||
@@ -235,6 +241,7 @@ func setupSubsonic(r *mux.Router, ctrl *ctrlsubsonic.Controller) {
|
||||
r.Handle("/getCoverArt{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeGetCoverArt))
|
||||
r.Handle("/stream{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeStream))
|
||||
r.Handle("/download{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeStream))
|
||||
r.Handle("/getAvatar{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeGetAvatar))
|
||||
|
||||
// browse by tag
|
||||
r.Handle("/getAlbum{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetAlbum))
|
||||
|
||||
Reference in New Issue
Block a user