feat(admin): update stylesheet
This commit is contained in:
6
go.mod
6
go.mod
@@ -8,6 +8,7 @@ require (
|
||||
github.com/dexterlb/mpvipc v0.0.0-20221227161445-38b9935eae9d
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.3.0
|
||||
@@ -26,9 +27,10 @@ require (
|
||||
github.com/oklog/run v1.1.0
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c
|
||||
github.com/peterbourgon/ff v1.7.1
|
||||
github.com/philippta/go-template v0.0.0-20220911145045-4556aca435e4
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be
|
||||
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981
|
||||
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
|
||||
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2
|
||||
golang.org/x/net v0.7.0
|
||||
gopkg.in/gormigrate.v1 v1.6.0
|
||||
)
|
||||
@@ -51,7 +53,7 @@ require (
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/mmcdole/goxpp v1.0.0 // indirect
|
||||
github.com/mmcdole/goxpp v1.1.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
golang.org/x/crypto v0.6.0 // indirect
|
||||
|
||||
12
go.sum
12
go.sum
@@ -27,6 +27,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
@@ -111,8 +113,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mmcdole/gofeed v1.2.0 h1:kuq7tJnDf0pnsDzF820ukuySHxFimAcizpG15gYHIns=
|
||||
github.com/mmcdole/gofeed v1.2.0/go.mod h1:TEyTG4gw4Q5Co+Hgahx/Oi3E0JHLM8BXtWC+mkJtRsw=
|
||||
github.com/mmcdole/goxpp v1.0.0 h1:/eu75G4jwH/LaugmPVB0FFC8LdKw00UMrpo6N7ym45o=
|
||||
github.com/mmcdole/goxpp v1.0.0/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
|
||||
github.com/mmcdole/goxpp v1.1.0 h1:WwslZNF7KNAXTFuzRtn/OKZxFLJAAyOA9w82mDz2ZGI=
|
||||
github.com/mmcdole/goxpp v1.1.0/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -131,6 +133,8 @@ github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwU
|
||||
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||
github.com/peterbourgon/ff v1.7.1 h1:xt1lxTG+Nr2+tFtysY7abFgPoH3Lug8CwYJMOmJRXhk=
|
||||
github.com/peterbourgon/ff v1.7.1/go.mod h1:fYI5YA+3RDqQRExmFbHnBjEeWzh9TrS8rnRpEq7XIg0=
|
||||
github.com/philippta/go-template v0.0.0-20220911145045-4556aca435e4 h1:SvVjZyjXBOjqjCdMiC9ndyWb709aP5qU4Qbun40GCxA=
|
||||
github.com/philippta/go-template v0.0.0-20220911145045-4556aca435e4/go.mod h1:Mpa6Hci7lO3vybfdYlWXmH5gEq2vyOmYYjhrlwCTW3w=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
||||
@@ -149,8 +153,8 @@ golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w=
|
||||
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI=
|
||||
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
|
||||
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
//nolint:gochecknoglobals,golint,stylecheck
|
||||
package assets
|
||||
|
||||
import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
//go:embed layouts
|
||||
var Layouts embed.FS
|
||||
|
||||
//go:embed pages
|
||||
var Pages embed.FS
|
||||
|
||||
//go:embed partials
|
||||
var Partials embed.FS
|
||||
|
||||
//go:embed static
|
||||
var Static embed.FS
|
||||
@@ -1,31 +0,0 @@
|
||||
{{ define "layout" }}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>gonic</title>
|
||||
{{ template "head" }}
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<div id="header">
|
||||
<a href="{{ path "/admin/home" }}">
|
||||
<img src="{{ path "/admin/static/gonic.png" }}">
|
||||
</a>
|
||||
</div>
|
||||
{{ range $flash := .Flashes }}
|
||||
<div class="padded flash-{{ $flash.Type }}">
|
||||
<i class="mdi mdi-alert-circle"></i> {{ $flash.Message }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ template "content" . }}
|
||||
<div class="padded-side text-right">
|
||||
<span class="text-light">{{ .Version }}</span>
|
||||
senan kelly, 2020
|
||||
<span class="text-light">|</span>
|
||||
<a href="https://github.com/sentriz/gonic">github</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
@@ -1,10 +0,0 @@
|
||||
{{ define "content" }}
|
||||
<div class="padded-side text-light text-right">
|
||||
welcome {{ .User.Name }}
|
||||
|
|
||||
<a href="{{ path "/admin/home" }}">home</a>
|
||||
|
|
||||
<a href="{{ path "/admin/logout" }}">logout <i class="mdi mdi-logout-variant"></i></a>
|
||||
</div>
|
||||
{{ template "user" . }}
|
||||
{{ end }}
|
||||
@@ -1,26 +0,0 @@
|
||||
{{ 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 }}
|
||||
@@ -1,26 +0,0 @@
|
||||
{{ 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 }}
|
||||
@@ -1,12 +0,0 @@
|
||||
{{ define "user" }}
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account-key"></i> changing account password
|
||||
</div>
|
||||
<form class="block" action="{{ path "/admin/change_own_password_do" }}" method="post">
|
||||
<input type="password" id="password_one" name="password_one" placeholder="new password">
|
||||
<input type="password" id="password_two" name="password_two" placeholder="verify new password">
|
||||
<input type="submit" value="change">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -1,11 +0,0 @@
|
||||
{{ define "user" }}
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account-key"></i> changing account username
|
||||
</div>
|
||||
<form class="block" action="{{ path "/admin/change_own_username_do" }}" method="post">
|
||||
<input type="text" id="username" name="username" placeholder="new username">
|
||||
<input type="submit" value="change">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -1,12 +0,0 @@
|
||||
{{ define "user" }}
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account-key"></i> changing {{ .SelectedUser.Name }}'s password
|
||||
</div>
|
||||
<form class="block" action="{{ printf "/admin/change_password_do?user=%s" .SelectedUser.Name | path }}" method="post">
|
||||
<input type="password" id="password_one" name="password_one" placeholder="new password">
|
||||
<input type="password" id="password_two" name="password_two" placeholder="verify new password">
|
||||
<input type="submit" value="change">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -1,11 +0,0 @@
|
||||
{{ define "user" }}
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account-key"></i> changing {{ .SelectedUser.Name }}'s username
|
||||
</div>
|
||||
<form class="block" action="{{ printf "/admin/change_username_do?user=%s" .SelectedUser.Name | path }}" method="post">
|
||||
<input type="text" id="username" name="username" placeholder="new username">
|
||||
<input type="submit" value="change">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -1,14 +0,0 @@
|
||||
{{ define "user" }}
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account-remove"></i> deleting user {{ .SelectedUser.Name }}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
are you sure?<br/>
|
||||
<span class="text-light">their plays, starred, etc. will also be deleted</span>
|
||||
</div>
|
||||
<form class="block" action="{{ printf "/admin/delete_user_do?user=%s" .SelectedUser.Name | path }}" method="post">
|
||||
<input type="submit" value="yes">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -1,280 +0,0 @@
|
||||
{{ define "user" }}
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-chart-arc"></i> stats
|
||||
</div>
|
||||
<div class="block-right">
|
||||
<table id="stats" class="text-right">
|
||||
<tr><td>artists:</td> <td>{{ .ArtistCount }}</td></tr>
|
||||
<tr><td>albums:</td> <td>{{ .AlbumCount }}</td></tr>
|
||||
<tr><td>tracks:</td> <td>{{ .TrackCount }}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-lastfm"></i> last.fm
|
||||
</div>
|
||||
<div class="box-description text-light">
|
||||
<p>gonic can scrobble to <a href="https://www.last.fm/" target="_blank">last.fm</a> for any user (but the admin must set a global api key)</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{ if .CurrentLastFMAPIKey }}
|
||||
<span class="text-light">current status</span>
|
||||
{{ if .User.LastFMSession }}
|
||||
<span>linked</span><br/>
|
||||
<form action="{{ path "/admin/unlink_lastfm_do" }}" method="post">
|
||||
<input type="submit" value="unlink">
|
||||
</form>
|
||||
{{ else }}
|
||||
<span class="angry">unlinked</span><br/>
|
||||
{{ $cbPath := path "/admin/link_lastfm_do" }}
|
||||
{{ $cbURL := printf "%s%s" .RequestRoot $cbPath }}
|
||||
<a href="https://www.last.fm/api/auth/?api_key={{ .CurrentLastFMAPIKey }}&cb={{ $cbURL }}">link…</a>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<p class="text-light">api key not set</p>
|
||||
{{ if not .User.IsAdmin }}
|
||||
<p class="text-light">please ask your admin to set it</p>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if .User.IsAdmin }}
|
||||
<p><a href="{{ path "/admin/update_lastfm_api_key" }}">update api key…</a></p>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-brain"></i> listenbrainz
|
||||
</div>
|
||||
<div class="box-description text-light">
|
||||
<p>gonic can scrobble to <a href="https://listenbrainz.org/" target="_blank">listenbrainz</a> and compatible sites</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="text-light">current status</span>
|
||||
{{ if .User.ListenBrainzToken }}
|
||||
<span>linked</span><br/>
|
||||
<form action="{{ path "/admin/unlink_listenbrainz_do" }}" method="post">
|
||||
<input type="submit" value="unlink">
|
||||
</form>
|
||||
{{ else }}
|
||||
<span class="angry">unlinked</span>
|
||||
<form class="block" action="{{ path "/admin/link_listenbrainz_do" }}" method="post">
|
||||
<input type="text" name="url" placeholder="server addr" value="{{ default .DefaultListenBrainzURL .User.ListenBrainzURL }}">
|
||||
<input type="text" name="token" placeholder="e199b311abd01f0d" value="{{ .User.ListenBrainzToken }}">
|
||||
<input type="submit" value="update">
|
||||
</form>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="padded box">
|
||||
{{ if .User.IsAdmin }}
|
||||
{{/* admin panel to manage all users */}}
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account-multiple"></i> users
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{{ range $user := .AllUsers }}
|
||||
<i>{{ $user.Name }}</i>
|
||||
<span class="text-light no-small">{{ $user.CreatedAt | date }}</span>
|
||||
<span class="text-light">|</span>
|
||||
<a href="{{ printf "/admin/change_username?user=%s" $user.Name | path }}">username…</a>
|
||||
<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 }}
|
||||
<a href="{{ printf "/admin/delete_user?user=%s" $user.Name | path }}">delete…</a>
|
||||
{{ end }}
|
||||
<br/>
|
||||
{{ end }}
|
||||
<a href="{{ path "/admin/create_user" }}" class="button">create new…</a>
|
||||
</div>
|
||||
{{ else }}
|
||||
{{/* user panel to manage themselves */}}
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account"></i> your account
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<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>
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-folder-multiple"></i> recent folders
|
||||
</div>
|
||||
<div class="block-right text-right">
|
||||
{{ if eq (len .RecentFolders) 0 }}
|
||||
<span class="text-light">no folders yet</span>
|
||||
{{ end }}
|
||||
<table id="recent-folders">
|
||||
<colgroup>
|
||||
<col width="80%" />
|
||||
<col width="0%" />
|
||||
</colgroup>
|
||||
{{ range $folder := .RecentFolders }}
|
||||
<tr>
|
||||
<td class="text-right text-trunc">{{ $folder.RightPath }}</td>
|
||||
<td><span class="text-light" title="{{ $folder.ModifiedAt }}">{{ $folder.ModifiedAt | dateHuman }}</span></td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
{{- if and (not .IsScanning) (.User.IsAdmin) -}}
|
||||
{{- if not .LastScanTime.IsZero -}}
|
||||
<p class="text-light" title="{{ .LastScanTime }}">scanned {{ .LastScanTime | dateHuman }}</p>
|
||||
{{ end }}
|
||||
<form action="{{ path "/admin/start_scan_inc_do" }}" method="post">
|
||||
<input type="submit" title="start a incremental scan" value="scan now">
|
||||
</form>
|
||||
<form action="{{ path "/admin/start_scan_full_do" }}" method="post">
|
||||
<input type="submit" title="start a full scan (takes longer, and shouldn't usually be necessary)" value="scan full (!)">
|
||||
</form>
|
||||
{{ end }}
|
||||
{{- if .IsScanning }}<p>scan in progress...</p>{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-file-music"></i> transcoding device profiles
|
||||
</div>
|
||||
<div class="box-description text-light">
|
||||
<p>you can find your device's client name in the gonic logs.</p>
|
||||
<p>some common client names are <span class="text-emp">DSub</span>, <span class="text-emp">Jamstash</span>, <span class="text-emp">Soundwaves</span>, or use <span class="text-emp">*</span> as fallback rule for any client.</p>
|
||||
<p>for more info, see <a href="https://github.com/sentriz/gonic/wiki/transcode-profiles" target="_blank">transcode profiles</a></p>
|
||||
</div>
|
||||
<div class="block-right">
|
||||
<table id="transcode-preferences">
|
||||
{{ range $pref := .TranscodePreferences }}
|
||||
<tr>
|
||||
{{ $formSuffix := kebabcase $pref.Client }}
|
||||
<form id="transcode-pref-{{ $formSuffix }}" action="{{ printf "/admin/delete_transcode_pref_do?client=%s" $pref.Client | path }}" method="post"></form>
|
||||
<td>{{ $pref.Client }}</td>
|
||||
<td>{{ $pref.Profile }}</td>
|
||||
<td><input form="transcode-pref-{{ $formSuffix }}" type="submit" value="delete"></td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
<tr>
|
||||
<form id="transcode-pref-add" action="{{ path "/admin/create_transcode_pref_do" }}" method="post"></form>
|
||||
<td><input form="transcode-pref-add" type="text" name="client" placeholder="client name"></td>
|
||||
<td><select form="transcode-pref-add" name="profile">
|
||||
{{ range $profile := .TranscodeProfiles }}
|
||||
<option value="{{ $profile }}">{{ $profile }}</option>
|
||||
{{ end }}
|
||||
</select></td>
|
||||
<td><input form="transcode-pref-add" type="submit" value="save"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{ if .User.IsAdmin }}
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-rss-box"></i> podcasts
|
||||
</div>
|
||||
<div class="box-description text-light">
|
||||
<p>you can add podcasts rss feeds here</p>
|
||||
</div>
|
||||
<div>
|
||||
<table id="podcast-preferences">
|
||||
{{ range $pref := .Podcasts }}
|
||||
<tr>
|
||||
<form id="podcast-{{ $pref.ID }}-download" action="{{ printf "/admin/download_podcast_do?id=%d" $pref.ID | path }}" method="post"></form>
|
||||
<form id="podcast-{{ $pref.ID }}-auto-download" action="{{ printf "/admin/update_podcast_do?id=%d" $pref.ID | path }}" method="post"></form>
|
||||
<form id="podcast-{{ $pref.ID }}-delete" action="{{ printf "/admin/delete_podcast_do?id=%d" $pref.ID | path }}" method="post"></form>
|
||||
<td class="text-full">{{ $pref.Title }}</td>
|
||||
<td><select class="no-small" form="podcast-{{ $pref.ID }}-auto-download" name="setting">
|
||||
{{ if eq $pref.AutoDownload "latest" }}
|
||||
<option value="latest" selected="selected">download latest</option>
|
||||
<option value="none">no auto download</option>
|
||||
{{ else }}
|
||||
<option value="none" selected="selected" >no auto download</option>
|
||||
<option value="latest">download latest</option>
|
||||
{{ end }}
|
||||
</select></td>
|
||||
<td><input class="no-small" form="podcast-{{ $pref.ID }}-download" type="submit" value="download all"></td>
|
||||
<td><input form="podcast-{{ $pref.ID }}-auto-download" type="submit" value="save"></td>
|
||||
<td><input form="podcast-{{ $pref.ID }}-delete" type="submit" value="delete"></td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
<tr>
|
||||
<form id="podcast-add" action="{{ path "/admin/add_podcast_do" }}" method="post"></form>
|
||||
<td><input form="podcast-add" type="text" name="feed" placeholder="rss feed url"></td>
|
||||
<td><input form="podcast-add" type="submit" value="save"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .User.IsAdmin }}
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-radio"></i> internet_radio_stations
|
||||
</div>
|
||||
<div class="box-description text-light">
|
||||
<p>you can add and update internet radio stations here</p>
|
||||
</div>
|
||||
<div>
|
||||
<table id="irs-preferences">
|
||||
{{ range $pref := .InternetRadioStations }}
|
||||
<tr>
|
||||
<form id="irs-{{ $pref.ID }}-update" action="{{ printf "/admin/update_internet_radio_station_do?id=%d" $pref.ID | path }}" method="post"></form>
|
||||
<form id="irs-{{ $pref.ID }}-delete" action="{{ printf "/admin/delete_internet_radio_station_do?id=%d" $pref.ID | path }}" method="post"></form>
|
||||
<td><input form="irs-{{ $pref.ID }}-update" type="text" name="streamURL" value={{ $pref.StreamURL }}></td>
|
||||
<td><input form="irs-{{ $pref.ID }}-update" type="text" name="name" value={{ $pref.Name }}></td>
|
||||
<td><input form="irs-{{ $pref.ID }}-update" type="text" name="homepageURL" value={{ $pref.HomepageURL }}></td>
|
||||
<td><input form="irs-{{ $pref.ID }}-update" type="submit" value="update"></td>
|
||||
<td><input form="irs-{{ $pref.ID }}-delete" type="submit" value="delete"></td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
<tr>
|
||||
<form id="irs-add" action="{{ path "/admin/add_internet_radio_station_do" }}" method="post"></form>
|
||||
<td><input form="irs-add" type="text" name="streamURL" placeholder="stream URL"></td>
|
||||
<td><input form="irs-add" type="text" name="name" placeholder="name"></td>
|
||||
<td><input form="irs-add" type="text" name="homepageURL" placeholder="[homepage URL]"></td>
|
||||
<td><input form="irs-add" type="submit" value="add"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-playlist-music"></i> playlists
|
||||
</div>
|
||||
<div class="block-right text-right">
|
||||
{{ if eq (len .Playlists) 0 }}
|
||||
<span class="text-light">no playlists yet</span>
|
||||
{{ end }}
|
||||
<table id="recent-playlists">
|
||||
{{ range $i, $playlist := .Playlists }}
|
||||
<tr>
|
||||
<form id="recent-playlists-{{ $i }}" action="{{ printf "/admin/delete_playlist_do?id=%d" $playlist.ID | path }}" method="post"></form>
|
||||
<td class="text-right">{{ $playlist.Name }}</td>
|
||||
<td><span class="text-light">({{ $playlist.TrackCount }} tracks)</span></td>
|
||||
<td class="no-small"><span class="text-light" title="{{ $playlist.CreatedAt }}">{{ $playlist.CreatedAt | dateHuman }}</span></td>
|
||||
<td><input form="recent-playlists-{{ $i }}" type="submit" value="delete"></td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
<form
|
||||
class="file-upload"
|
||||
enctype="multipart/form-data"
|
||||
action="{{ path "/admin/upload_playlist_do" }}"
|
||||
method="post"
|
||||
>
|
||||
<div style="position: relative;">
|
||||
<input style="position: absolute; opacity: 0;" name="playlist-files" type="file" multiple />
|
||||
<input type="button" value="upload m3u8">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -1,12 +0,0 @@
|
||||
{{ define "content" }}
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-login-variant"></i> login
|
||||
</div>
|
||||
<form class="block" action="{{ path "/admin/login_do" }}" method="post">
|
||||
<input type="text" id="username" name="username" placeholder="username">
|
||||
<input type="password" id="password" name="password" placeholder="password">
|
||||
<input type="submit" value="login">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -1,7 +0,0 @@
|
||||
{{ define "content" }}
|
||||
<div class="padded">
|
||||
<div class="text-right">
|
||||
page not found
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -1,19 +0,0 @@
|
||||
{{ define "user" }}
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-key-change"></i> updating last.fm keys
|
||||
</div>
|
||||
<div class="box-description text-light">
|
||||
<p>you can get an api key <a href="https://www.last.fm/api/account/create" target="_blank">here</a> (note: only the "application name" field is required)</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p><span class="text-light">current key</span> <i>{{ default "not set" .CurrentLastFMAPIKey }}</i></p>
|
||||
<p><span class="text-light">current secret</span> <i>{{ default "not set" .CurrentLastFMAPISecret }}</i></p>
|
||||
</div>
|
||||
<form class="block" action="{{ path "/admin/update_lastfm_api_key_do" }}" method="post">
|
||||
<input type="text" id="api_key" name="api_key" placeholder="new key">
|
||||
<input type="text" id="secret" name="secret" placeholder="new secret">
|
||||
<input type="submit" value="update">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -1,9 +0,0 @@
|
||||
{{ define "head" }}
|
||||
<link rel="stylesheet" href="{{ path "/admin/static/reset.css" }}">
|
||||
<link rel="stylesheet" href="https://cdn.materialdesignicons.com/3.6.95/css/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="{{ path "/admin/static/main.css" | noCache }}">
|
||||
<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 }}
|
||||
@@ -1,186 +0,0 @@
|
||||
:root {
|
||||
--size: 13px;
|
||||
--width-body: 750px;
|
||||
--width-box-description: 510px;
|
||||
--width-form: 400px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 780px) {
|
||||
:root {
|
||||
--size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
body,
|
||||
button,
|
||||
div,
|
||||
i,
|
||||
input[type],
|
||||
select,
|
||||
span,
|
||||
html {
|
||||
font-family: monospace;
|
||||
font-size: var(--size);
|
||||
color: black;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
body {
|
||||
max-width: var(--width-body);
|
||||
margin: 0 auto;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
input[type],
|
||||
select,
|
||||
button,
|
||||
textarea {
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
outline: 1px solid #ccc;
|
||||
height: var(--size);
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
input[type] {
|
||||
width: calc(var(--size) * 9);
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
form.block {
|
||||
max-width: var(--width-form);
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
form > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table td {
|
||||
padding-left: calc(var(--size) * 0.5);
|
||||
}
|
||||
|
||||
table td.text-trunc {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: #0064c1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#content > * {
|
||||
margin: calc(var(--size) * 1.5);
|
||||
}
|
||||
|
||||
#header {
|
||||
border-bottom: 2px solid #0000001a;
|
||||
}
|
||||
|
||||
#header img {
|
||||
width: 60%;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.flash-warning {
|
||||
background-color: #fd1b1b1c;
|
||||
border-right: 2px solid #fd1b1b1c;
|
||||
border-bottom: 2px solid #fd1b1b1c;
|
||||
}
|
||||
|
||||
.flash-normal {
|
||||
background-color: #15ff5424;
|
||||
border-right: 2px solid #15ff5424;
|
||||
border-bottom: 2px solid #15ff5424;
|
||||
}
|
||||
|
||||
.box {
|
||||
background-color: #00000005;
|
||||
border-right: 2px solid #0000000c;
|
||||
border-bottom: 2px solid #0000000c;
|
||||
}
|
||||
|
||||
.box-description {
|
||||
max-width: var(--width-box-description);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 780px) {
|
||||
.no-small {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-light {
|
||||
color: #00000082;
|
||||
}
|
||||
|
||||
.text-emp {
|
||||
font-style: italic;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.text-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.block-right > * {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.padded {
|
||||
padding: var(--size);
|
||||
}
|
||||
|
||||
.padded-side {
|
||||
padding: 0 var(--size);
|
||||
}
|
||||
|
||||
.angry {
|
||||
background-color: #f4433669;
|
||||
}
|
||||
|
||||
.avatar-preview {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: cover;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
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,135 +0,0 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html,
|
||||
body,
|
||||
div,
|
||||
span,
|
||||
applet,
|
||||
object,
|
||||
iframe,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
blockquote,
|
||||
pre,
|
||||
a,
|
||||
abbr,
|
||||
acronym,
|
||||
address,
|
||||
big,
|
||||
cite,
|
||||
code,
|
||||
del,
|
||||
dfn,
|
||||
em,
|
||||
img,
|
||||
ins,
|
||||
kbd,
|
||||
q,
|
||||
s,
|
||||
samp,
|
||||
small,
|
||||
strike,
|
||||
strong,
|
||||
sub,
|
||||
sup,
|
||||
tt,
|
||||
var,
|
||||
b,
|
||||
u,
|
||||
i,
|
||||
center,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ol,
|
||||
ul,
|
||||
li,
|
||||
fieldset,
|
||||
form,
|
||||
label,
|
||||
legend,
|
||||
table,
|
||||
caption,
|
||||
tbody,
|
||||
tfoot,
|
||||
thead,
|
||||
tr,
|
||||
th,
|
||||
td,
|
||||
article,
|
||||
aside,
|
||||
canvas,
|
||||
details,
|
||||
embed,
|
||||
figure,
|
||||
figcaption,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
output,
|
||||
ruby,
|
||||
section,
|
||||
summary,
|
||||
time,
|
||||
mark,
|
||||
audio,
|
||||
video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* html5 display-role reset for older browsers */
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
menu,
|
||||
nav,
|
||||
section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
blockquote,
|
||||
q {
|
||||
quotes: none;
|
||||
}
|
||||
|
||||
blockquote:before,
|
||||
blockquote:after,
|
||||
q:before,
|
||||
q:after {
|
||||
content: "";
|
||||
content: none;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
10
server/ctrladmin/adminui/adminui.go
Normal file
10
server/ctrladmin/adminui/adminui.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package adminui
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed components.tmpl pages/*.tmpl
|
||||
var TemplatesFS embed.FS
|
||||
|
||||
//go:generate npx tailwindcss@v3.2.4 --config tailwind.config.js --input style.css --output static/style.css --minify
|
||||
//go:embed static/*
|
||||
var StaticFS embed.FS
|
||||
107
server/ctrladmin/adminui/components.tmpl
Normal file
107
server/ctrladmin/adminui/components.tmpl
Normal file
@@ -0,0 +1,107 @@
|
||||
{{ define "block" }}
|
||||
<div class="border-b-2 border-r-2 border-gray-300/80 bg-gray-50 p-4">
|
||||
<div class="inline-flex items-center gap-x-3">
|
||||
<span class="text-gray-500/80 w-4">{{ component "icon" .Props.Icon }}{{ end }}</span>
|
||||
<span class="text-gray-900 font-bold">{{ .Props.Name }}</span>
|
||||
</div>
|
||||
<hr class="bg-gray-900/30 my-1" />
|
||||
{{ if .Props.Desc }}
|
||||
<div class="font-medium text-gray-500 max-w-[700px]">{{ .Props.Desc }}</div>
|
||||
{{ end }}
|
||||
<div class="mt-3 max-w-[700px] ml-auto space-y-2 text-right">
|
||||
{{ slot }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ define "link" }}
|
||||
<a class="text-blue-500" href="{{ .Props.To }}">{{ slot }}<span class="hidden md:inline">…</span></a>
|
||||
{{ end }}
|
||||
|
||||
{{ define "ext_link" }}
|
||||
<a class="text-blue-500" href="{{ .Props.To }}" target="_blank" rel="noopener noreferrer">{{ slot }}</a>
|
||||
{{ end }}
|
||||
|
||||
{{ define "layout_user" }}
|
||||
<div class="px-4 text-gray-500 text-right whitespace-nowrap">
|
||||
welcome {{ .User.Name }}
|
||||
|
|
||||
{{ component "link" (props . "To" (path "/admin/home")) }}home{{ end }}
|
||||
|
|
||||
{{ component "link" (props . "To" (path "/admin/logout")) }}logout{{ end }}
|
||||
</div>
|
||||
{{ slot }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "layout" }}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>gonic</title>
|
||||
<link rel="stylesheet" href="{{ path "/admin/static/style.css" | noCache }}">
|
||||
<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>
|
||||
</head>
|
||||
<body class="font-mono leading-4 text-base text-gray-800">
|
||||
<div class="container mx-auto min-w-min space-y-5 p-5">
|
||||
<div class="border-b-2 border-gray-300">
|
||||
<a href="{{ path "/admin/home" }}">
|
||||
<img class="mx-auto w-[400px]" src="{{ path "/admin/static/gonic.png" }}">
|
||||
</a>
|
||||
</div>
|
||||
{{ range $flash := .Flashes }}
|
||||
{{ $colour := "bg-green-200" }}
|
||||
{{ if eq $flash.Type "warning" }}{{ $colour = "bg-red-200" }}{{ end }}
|
||||
<div class="p-4 shadow-sm {{ $colour }} inline-flex items-center gap-x-3 w-full">
|
||||
<span class="text-gray-500/80 w-5">{{ component "icon" "circle-info" }}{{ end }}</span>
|
||||
<span>{{ $flash.Message }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ slot }}
|
||||
<div class="px-5 text-right whitespace-nowrap">
|
||||
<span class="text-gray-500">{{ .Version }}</span>
|
||||
senan kelly, 2020
|
||||
<span class="text-gray-500">|</span>
|
||||
{{ component "ext_link" (props . "To" "https://github.com/sentriz/gonic") }}github{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
|
||||
|
||||
{{/* from https://github.com/FortAwesome/Font-Awesome/tree/6.x/svgs/brand */}}
|
||||
{{/* TODO: see if we can dynamically render templates based on a variable instead of this */}}
|
||||
|
||||
{{ define "icon" }}
|
||||
{{ if (eq . "lastfm") }}
|
||||
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M225.8 367.1l-18.8-51s-30.5 34-76.2 34c-40.5 0-69.2-35.2-69.2-91.5 0-72.1 36.4-97.9 72.1-97.9 66.5 0 74.8 53.3 100.9 134.9 18.8 56.9 54 102.6 155.4 102.6 72.7 0 122-22.3 122-80.9 0-72.9-62.7-80.6-115-92.1-25.8-5.9-33.4-16.4-33.4-34 0-19.9 15.8-31.7 41.6-31.7 28.2 0 43.4 10.6 45.7 35.8l58.6-7c-4.7-52.8-41.1-74.5-100.9-74.5-52.8 0-104.4 19.9-104.4 83.9 0 39.9 19.4 65.1 68 76.8 44.9 10.6 79.8 13.8 79.8 45.7 0 21.7-21.1 30.5-61 30.5-59.2 0-83.9-31.1-97.9-73.9-32-96.8-43.6-163-161.3-163C45.7 113.8 0 168.3 0 261c0 89.1 45.7 137.2 127.9 137.2 66.2 0 97.9-31.1 97.9-31.1z"/></svg>
|
||||
{{ else if (eq . "brain" ) }}
|
||||
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M184 0c30.9 0 56 25.1 56 56V456c0 30.9-25.1 56-56 56c-28.9 0-52.7-21.9-55.7-50.1c-5.2 1.4-10.7 2.1-16.3 2.1c-35.3 0-64-28.7-64-64c0-7.4 1.3-14.6 3.6-21.2C21.4 367.4 0 338.2 0 304c0-31.9 18.7-59.5 45.8-72.3C37.1 220.8 32 207 32 192c0-30.7 21.6-56.3 50.4-62.6C80.8 123.9 80 118 80 112c0-29.9 20.6-55.1 48.3-62.1C131.3 21.9 155.1 0 184 0zM328 0c28.9 0 52.6 21.9 55.7 49.9c27.8 7 48.3 32.1 48.3 62.1c0 6-.8 11.9-2.4 17.4c28.8 6.2 50.4 31.9 50.4 62.6c0 15-5.1 28.8-13.8 39.7C493.3 244.5 512 272.1 512 304c0 34.2-21.4 63.4-51.6 74.8c2.3 6.6 3.6 13.8 3.6 21.2c0 35.3-28.7 64-64 64c-5.6 0-11.1-.7-16.3-2.1c-3 28.2-26.8 50.1-55.7 50.1c-30.9 0-56-25.1-56-56V56c0-30.9 25.1-56 56-56z"/></svg>
|
||||
{{ else if (eq . "chart-pie" ) }}
|
||||
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M302 240V16.6c0-9 7-16.6 16-16.6C441.7 0 542 100.3 542 224c0 9-7.6 16-16.6 16H302zM30 272C30 150.7 120.1 50.3 237 34.3c9.2-1.3 17 6.1 17 15.4V288L410.5 444.5c6.7 6.7 6.2 17.7-1.5 23.1C369.8 495.6 321.8 512 270 512C137.5 512 30 404.6 30 272zm526.4 16c9.3 0 16.6 7.8 15.4 17c-7.7 55.9-34.6 105.6-73.9 142.3c-6 5.6-15.4 5.2-21.2-.7L318 288H556.4z"/></svg>
|
||||
{{ else if (eq . "brain" ) }}
|
||||
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M184 0c30.9 0 56 25.1 56 56V456c0 30.9-25.1 56-56 56c-28.9 0-52.7-21.9-55.7-50.1c-5.2 1.4-10.7 2.1-16.3 2.1c-35.3 0-64-28.7-64-64c0-7.4 1.3-14.6 3.6-21.2C21.4 367.4 0 338.2 0 304c0-31.9 18.7-59.5 45.8-72.3C37.1 220.8 32 207 32 192c0-30.7 21.6-56.3 50.4-62.6C80.8 123.9 80 118 80 112c0-29.9 20.6-55.1 48.3-62.1C131.3 21.9 155.1 0 184 0zM328 0c28.9 0 52.6 21.9 55.7 49.9c27.8 7 48.3 32.1 48.3 62.1c0 6-.8 11.9-2.4 17.4c28.8 6.2 50.4 31.9 50.4 62.6c0 15-5.1 28.8-13.8 39.7C493.3 244.5 512 272.1 512 304c0 34.2-21.4 63.4-51.6 74.8c2.3 6.6 3.6 13.8 3.6 21.2c0 35.3-28.7 64-64 64c-5.6 0-11.1-.7-16.3-2.1c-3 28.2-26.8 50.1-55.7 50.1c-30.9 0-56-25.1-56-56V56c0-30.9 25.1-56 56-56z"/></svg>
|
||||
{{ else if (eq . "users" ) }}
|
||||
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M144 0a80 80 0 1 1 0 160A80 80 0 1 1 144 0zM512 0a80 80 0 1 1 0 160A80 80 0 1 1 512 0zM0 298.7C0 239.8 47.8 192 106.7 192h42.7c15.9 0 31 3.5 44.6 9.7c-1.3 7.2-1.9 14.7-1.9 22.3c0 38.2 16.8 72.5 43.3 96c-.2 0-.4 0-.7 0H21.3C9.6 320 0 310.4 0 298.7zM405.3 320c-.2 0-.4 0-.7 0c26.6-23.5 43.3-57.8 43.3-96c0-7.6-.7-15-1.9-22.3c13.6-6.3 28.7-9.7 44.6-9.7h42.7C592.2 192 640 239.8 640 298.7c0 11.8-9.6 21.3-21.3 21.3H405.3zM224 224a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zM128 485.3C128 411.7 187.7 352 261.3 352H378.7C452.3 352 512 411.7 512 485.3c0 14.7-11.9 26.7-26.7 26.7H154.7c-14.7 0-26.7-11.9-26.7-26.7z"/></svg>
|
||||
{{ else if (eq . "user" ) }}
|
||||
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512H418.3c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304H178.3z"/></svg>
|
||||
{{ else if (eq . "folder-tree" ) }}
|
||||
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M64 32C64 14.3 49.7 0 32 0S0 14.3 0 32v96V384c0 35.3 28.7 64 64 64H256V384H64V160H256V96H64V32zM288 192c0 17.7 14.3 32 32 32H544c17.7 0 32-14.3 32-32V64c0-17.7-14.3-32-32-32H445.3c-8.5 0-16.6-3.4-22.6-9.4L409.4 9.4c-6-6-14.1-9.4-22.6-9.4H320c-17.7 0-32 14.3-32 32V192zm0 288c0 17.7 14.3 32 32 32H544c17.7 0 32-14.3 32-32V352c0-17.7-14.3-32-32-32H445.3c-8.5 0-16.6-3.4-22.6-9.4l-13.3-13.3c-6-6-14.1-9.4-22.6-9.4H320c-17.7 0-32 14.3-32 32V480z"/></svg>
|
||||
{{ else if (eq . "music" ) }}
|
||||
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M499.1 6.3c8.1 6 12.9 15.6 12.9 25.7v72V368c0 44.2-43 80-96 80s-96-35.8-96-80s43-80 96-80c11.2 0 22 1.6 32 4.6V147L192 223.8V432c0 44.2-43 80-96 80s-96-35.8-96-80s43-80 96-80c11.2 0 22 1.6 32 4.6V200 128c0-14.1 9.3-26.6 22.8-30.7l320-96c9.7-2.9 20.2-1.1 28.3 5z"/></svg>
|
||||
{{ else if (eq . "rss" ) }}
|
||||
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M0 64C0 46.3 14.3 32 32 32c229.8 0 416 186.2 416 416c0 17.7-14.3 32-32 32s-32-14.3-32-32C384 253.6 226.4 96 32 96C14.3 96 0 81.7 0 64zM0 416a64 64 0 1 1 128 0A64 64 0 1 1 0 416zM32 160c159.1 0 288 128.9 288 288c0 17.7-14.3 32-32 32s-32-14.3-32-32c0-123.7-100.3-224-224-224c-17.7 0-32-14.3-32-32s14.3-32 32-32z"/></svg>
|
||||
{{ else if (eq . "radio" ) }}
|
||||
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M494.8 47c12.7-3.7 20-17.1 16.3-29.8S494-2.8 481.2 1L51.7 126.9c-9.4 2.7-17.9 7.3-25.1 13.2C10.5 151.7 0 170.6 0 192v4V304 448c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V192c0-35.3-28.7-64-64-64H218.5L494.8 47zM368 240a80 80 0 1 1 0 160 80 80 0 1 1 0-160zM80 256c0-8.8 7.2-16 16-16h96c8.8 0 16 7.2 16 16s-7.2 16-16 16H96c-8.8 0-16-7.2-16-16zM64 320c0-8.8 7.2-16 16-16H208c8.8 0 16 7.2 16 16s-7.2 16-16 16H80c-8.8 0-16-7.2-16-16zm16 64c0-8.8 7.2-16 16-16h96c8.8 0 16 7.2 16 16s-7.2 16-16 16H96c-8.8 0-16-7.2-16-16z"/></svg>
|
||||
{{ else if (eq . "list" ) }}
|
||||
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M40 48C26.7 48 16 58.7 16 72v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V72c0-13.3-10.7-24-24-24H40zM192 64c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zm0 160c-17.7 0-32 14.3-32 32s14.3 32 32 32H480c17.7 0 32-14.3 32-32s-14.3-32-32-32H192zM16 232v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V232c0-13.3-10.7-24-24-24H40c-13.3 0-24 10.7-24 24zM40 368c-13.3 0-24 10.7-24 24v48c0 13.3 10.7 24 24 24H88c13.3 0 24-10.7 24-24V392c0-13.3-10.7-24-24-24H40z"/></svg>
|
||||
{{ else if (eq . "circle-info" ) }}
|
||||
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/></svg>
|
||||
{{ else if (eq . "key" ) }}
|
||||
<svg class="fill-current aspect-square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2023 Fonticons, Inc. --><path d="M336 352c97.2 0 176-78.8 176-176S433.2 0 336 0S160 78.8 160 176c0 18.7 2.9 36.8 8.3 53.7L7 391c-4.5 4.5-7 10.6-7 17v80c0 13.3 10.7 24 24 24h80c13.3 0 24-10.7 24-24V448h40c13.3 0 24-10.7 24-24V384h40c6.4 0 12.5-2.5 17-7l33.3-33.3c16.9 5.4 35 8.3 53.7 8.3zM376 96a40 40 0 1 1 0 80 40 40 0 1 1 0-80z"/></svg>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
25
server/ctrladmin/adminui/pages/change_avatar.tmpl
Normal file
25
server/ctrladmin/adminui/pages/change_avatar.tmpl
Normal file
@@ -0,0 +1,25 @@
|
||||
{{ component "layout" . }}
|
||||
{{ component "layout_user" . }}
|
||||
|
||||
{{ component "block" (props .
|
||||
"Icon" "user"
|
||||
"Name" (printf "changing %s's avatar" .SelectedUser.Name)
|
||||
) }}
|
||||
<div class="flex flex-col gap-2 items-end">
|
||||
{{ if ne (len .SelectedUser.Avatar) 0 }}
|
||||
<img class="h-[8rem] w-[8rem] object-cover" src="data:image/jpg;base64,{{ .SelectedUser.Avatar | base64 }}" />
|
||||
<form class="contents" action="{{ printf "/admin/delete_avatar_do?user=%s" .SelectedUser.Name | path }}" method="post">
|
||||
<input type="submit" value="delete avatar">
|
||||
</form>
|
||||
{{ end }}
|
||||
<form enctype="multipart/form-data" action="{{ printf "/admin/change_avatar_do?user=%s" .SelectedUser.Name | path }}" method="post">
|
||||
<div class="relative pointer-events-auto">
|
||||
<input class="auto-submit absolute opacity-0" name="avatar" type="file" accept="image/jpeg image/png image/gif" />
|
||||
<input type="button" value="choose file">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
16
server/ctrladmin/adminui/pages/change_password.tmpl
Normal file
16
server/ctrladmin/adminui/pages/change_password.tmpl
Normal file
@@ -0,0 +1,16 @@
|
||||
{{ component "layout" . }}
|
||||
{{ component "layout_user" . }}
|
||||
|
||||
{{ component "block" (props .
|
||||
"Icon" "user"
|
||||
"Name" (printf "changing %s's password" .SelectedUser.Name)
|
||||
) }}
|
||||
<form class="flex flex-col gap-2 items-end" action="{{ printf "/admin/change_password_do?user=%s" .SelectedUser.Name | path }}" method="post">
|
||||
<input type="password" id="password_one" name="password_one" placeholder="new password">
|
||||
<input type="password" id="password_two" name="password_two" placeholder="verify new password">
|
||||
<input type="submit" value="change">
|
||||
</form>
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
15
server/ctrladmin/adminui/pages/change_username.tmpl
Normal file
15
server/ctrladmin/adminui/pages/change_username.tmpl
Normal file
@@ -0,0 +1,15 @@
|
||||
{{ component "layout" . }}
|
||||
{{ component "layout_user" . }}
|
||||
|
||||
{{ component "block" (props .
|
||||
"Icon" "user"
|
||||
"Name" (printf "changing %s's username" .SelectedUser.Name)
|
||||
) }}
|
||||
<form class="flex flex-col md:flex-row gap-2 items-end" action="{{ printf "/admin/change_username_do?user=%s" .SelectedUser.Name | path }}" method="post">
|
||||
<input type="text" id="username" name="username" placeholder="new username">
|
||||
<input type="submit" value="change">
|
||||
</form>
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
@@ -1,13 +1,17 @@
|
||||
{{ define "user" }}
|
||||
<div class="padded box">
|
||||
<div class="box-title">
|
||||
<i class="mdi mdi-account-plus"></i> creating new user
|
||||
</div>
|
||||
<form class="block" action="{{ path "/admin/create_user_do" }}" method="post">
|
||||
{{ component "layout" . }}
|
||||
{{ component "layout_user" . }}
|
||||
|
||||
{{ component "block" (props .
|
||||
"Icon" "user"
|
||||
"Name" "creating new user"
|
||||
) }}
|
||||
<form class="flex flex-col gap-2 items-end" action="{{ path "/admin/create_user_do" }}" method="post">
|
||||
<input type="text" id="username" name="username" placeholder="username">
|
||||
<input type="password" id="password_one" name="password_one" placeholder="password">
|
||||
<input type="password" id="password_two" name="password_two" placeholder="verify password">
|
||||
<input type="submit" value="create">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
15
server/ctrladmin/adminui/pages/delete_user.tmpl
Normal file
15
server/ctrladmin/adminui/pages/delete_user.tmpl
Normal file
@@ -0,0 +1,15 @@
|
||||
{{ component "layout" . }}
|
||||
{{ component "layout_user" . }}
|
||||
|
||||
{{ component "block" (props .
|
||||
"Icon" "user"
|
||||
"Name" (printf "deleting user %s" .SelectedUser.Name)
|
||||
"Desc" "are you sure? this will also delete their plays, playlists, starred, rated, etc."
|
||||
) }}
|
||||
<form class="inline-block" action="{{ printf "/admin/delete_user_do?user=%s" .SelectedUser.Name | path }}" method="post">
|
||||
<input type="submit" value="yes">
|
||||
</form>
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
226
server/ctrladmin/adminui/pages/home.tmpl
Normal file
226
server/ctrladmin/adminui/pages/home.tmpl
Normal file
@@ -0,0 +1,226 @@
|
||||
{{ component "layout" . }}
|
||||
{{ component "layout_user" . }}
|
||||
|
||||
{{ component "block" (props .
|
||||
"Icon" "chart-pie"
|
||||
"Name" "stats"
|
||||
"Desc" "total items found in all watched folders"
|
||||
) }}
|
||||
<div class="grid grid-cols-[auto_min-content] gap-2 gap-x-5 text-right">
|
||||
<div class="text-gray-500">artists</div>
|
||||
<div class="font-bold">{{ .ArtistCount }}</div>
|
||||
<div class="text-gray-500">albums</div>
|
||||
<div class="font-bold">{{ .AlbumCount }}</div>
|
||||
<div class="text-gray-500">tracks</div>
|
||||
<div class="font-bold">{{ .TrackCount }}</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ component "block" (props .
|
||||
"Icon" "users"
|
||||
"Name" "user management"
|
||||
"Desc" "manage user accounts for subsonic api and web interface access"
|
||||
) }}
|
||||
<div class="grid grid-cols-[repeat(3,auto)_max-content] md:grid-cols-[auto_repeat(5,min-content)] gap-2 gap-x-5 items-center text-right">
|
||||
{{ range $user := .AllUsers }}
|
||||
<div class="col-span-3 md:col-auto ellipsis">{{ $user.Name }}</div>
|
||||
<div class="text-gray-500 whitespace-nowrap">{{ $user.CreatedAt | date }}</div>
|
||||
{{ component "link" (props . "To" (printf "/admin/change_username?user=%s" $user.Name | path)) }}username{{ end }}
|
||||
{{ component "link" (props . "To" (printf "/admin/change_password?user=%s" $user.Name | path)) }}password{{ end }}
|
||||
{{ component "link" (props . "To" (printf "/admin/change_avatar?user=%s" $user.Name | path)) }}avatar{{ end }}
|
||||
{{ if $user.IsAdmin }}
|
||||
<div class="text-gray-500">delete<span class="hidden md:inline">…</span></div>
|
||||
{{ else }}
|
||||
{{ component "link" (props . "To" (printf "/admin/delete_user?user=%s" $user.Name | path)) }}delete{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if .User.IsAdmin }}
|
||||
<div class="col-span-full">{{ component "link" (props . "To" (path "/admin/create_user")) }}create new{{ end }}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ component "block" (props .
|
||||
"Icon" "folder-tree"
|
||||
"Name" "recent folders"
|
||||
) }}
|
||||
<div class="grid grid-cols-[1fr,auto] gap-x-3 gap-y-2 items-center justify-items-end">
|
||||
{{ if eq (len .RecentFolders) 0 }}
|
||||
<div class="col-span-full text-gray-500">no folders yet</div>
|
||||
{{ end }}
|
||||
{{ range $folder := .RecentFolders }}
|
||||
<div class="text-left ellipsis">{{ $folder.RightPath }}</div>
|
||||
<div class="text-gray-500" title="{{ $folder.ModifiedAt }}">{{ $folder.ModifiedAt | dateHuman }}</div>
|
||||
{{ end }}
|
||||
{{ if and (not .IsScanning) (.User.IsAdmin) }}
|
||||
{{ if not .LastScanTime.IsZero }}
|
||||
<p class="col-span-full text-gray-500" title="{{ .LastScanTime }}">scanned {{ .LastScanTime | dateHuman }}</p>
|
||||
{{ end }}
|
||||
<form class="col-span-full" action="{{ path "/admin/start_scan_inc_do" }}" method="post">
|
||||
<input type="submit" title="start a incremental scan" value="scan now">
|
||||
</form>
|
||||
{{ end }}
|
||||
{{ if .IsScanning }}<p class="text-green-500 col-span-full">scan in progress...</p>{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ component "block" (props .
|
||||
"Icon" "music"
|
||||
"Name" "transcoding device profiles"
|
||||
"Desc" "you can find your device's client name in the gonic logs. some common client names are <span class='italic text-gray-800'>DSub</span>, <span class='italic text-gray-800'>Jamstash</span>, <span class=\"italic text-gray-800\">Soundwaves</span>, or use <span class=\"italic text-gray-800\">*</span> as fallback rule for any client. see the \"transcoding profiles\" page on the wiki for more info"
|
||||
) }}
|
||||
<div class="grid grid-cols-[1fr_1fr_auto] gap-2 items-center justify-items-end">
|
||||
{{ range $pref := .TranscodePreferences }}
|
||||
{{ $formSuffix := kebabcase $pref.Client }}
|
||||
<div class="ellipsis">{{ $pref.Client }}</div>
|
||||
<div>{{ $pref.Profile }}</div>
|
||||
<form class="contents" action="{{ printf "/admin/delete_transcode_pref_do?client=%s" $pref.Client | path }}" method="post">
|
||||
<input type="submit" value="delete">
|
||||
</form>
|
||||
{{ end }}
|
||||
<form class="contents" action="{{ path "/admin/create_transcode_pref_do" }}" method="post">
|
||||
<input type="text" name="client" placeholder="client name">
|
||||
<select name="profile">
|
||||
{{ range $profile := .TranscodeProfiles }}<option value="{{ $profile }}">{{ $profile }}</option>{{ end }}
|
||||
</select>
|
||||
<input type="submit" value="save">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ component "block" (props .
|
||||
"Icon" "lastfm"
|
||||
"Name" "last.fm"
|
||||
"Desc" "scrobble to last.fm on a per user basis (the admin must set a global api key first)"
|
||||
) }}
|
||||
<div class="flex flex-col gap-2 items-end">
|
||||
{{ if .CurrentLastFMAPIKey }}
|
||||
{{ if .User.LastFMSession }}
|
||||
<p class="text-gray-500">current status <span class="font-bold text-green-500">linked</span></p>
|
||||
<form class="contents" action="{{ path "/admin/unlink_lastfm_do" }}" method="post">
|
||||
<input type="submit" value="unlink">
|
||||
</form>
|
||||
{{ else }}
|
||||
<p class="text-gray-500">current status <span class="font-bold text-red-400">unlinked</span></p>
|
||||
{{ $cbPath := path "/admin/link_lastfm_do" }}
|
||||
{{ $cbURL := printf "%s%s" .RequestRoot $cbPath }}
|
||||
<div>{{ component "link" (props . "To" (printf "https://www.last.fm/api/auth/?api_key=%s&cb=%s" .CurrentLastFMAPIKey $cbURL)) }}link{{ end }}</div>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<p class="font-bold">api key not set</p>
|
||||
{{ if not .User.IsAdmin }}
|
||||
<p class="text-gray-500">please ask your admin to set it</p>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if .User.IsAdmin }}
|
||||
<p>{{ component "link" (props . "To" (path "/admin/update_lastfm_api_key" )) }}update api key{{ end }}</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ component "block" (props .
|
||||
"Icon" "brain"
|
||||
"Name" "listenbrainz"
|
||||
"Desc" "scrobble to listenbrainz and compatible sites on a per user basis"
|
||||
) }}
|
||||
<div class="grid grid-cols-[1fr_1fr_auto] gap-2 items-center justify-items-end">
|
||||
{{ if .User.ListenBrainzToken }}
|
||||
<p class="text-gray-500 col-span-full">current status <span class="font-bold text-green-500">linked</span></p>
|
||||
<form class="contents" action="{{ path "/admin/unlink_listenbrainz_do" }}" method="post">
|
||||
<input class="col-span-full" type="submit" value="unlink">
|
||||
</form>
|
||||
{{ else }}
|
||||
<p class="text-gray-500 col-span-full">current status <span class="font-bold text-red-400">unlinked</span></p>
|
||||
<form class="contents" action="{{ path "/admin/link_listenbrainz_do" }}" method="post">
|
||||
<input type="text" name="url" placeholder="server addr" value="{{ default .DefaultListenBrainzURL .User.ListenBrainzURL }}">
|
||||
<input type="text" name="token" placeholder="api key" value="{{ .User.ListenBrainzToken }}">
|
||||
<input type="submit" value="update">
|
||||
</form>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if .User.IsAdmin }}
|
||||
{{ component "block" (props .
|
||||
"Icon" "rss"
|
||||
"Name" "podcasts"
|
||||
"Desc" "you can add podcasts rss feeds here"
|
||||
) }}
|
||||
<div class="grid grid-cols-[auto_auto_min-content] md:grid-cols-[5fr_3fr_auto_auto] gap-2 items-center justify-items-end">
|
||||
{{ range $pref := .Podcasts }}
|
||||
<div class="ellipsis">{{ $pref.Title }}</div>
|
||||
<form class="contents" action="{{ printf "/admin/update_podcast_do?id=%d" $pref.ID | path }}" method="post">
|
||||
<select class="auto-submit" name="setting">
|
||||
<option value="latest" {{ if eq $pref.AutoDownload "latest" }}selected="selected"{{ end }}>download latest</option>
|
||||
<option value="none" {{ if eq (default "none" $pref.AutoDownload) "none" }}selected="selected"{{ end }}>no auto download</option>
|
||||
</select>
|
||||
</form>
|
||||
<form class="hidden md:contents" action="{{ printf "/admin/download_podcast_do?id=%d" $pref.ID | path }}" method="post">
|
||||
<input type="submit" value="download all">
|
||||
</form>
|
||||
<form class="contents" action="{{ printf "/admin/delete_podcast_do?id=%d" $pref.ID | path }}" method="post">
|
||||
<input type="submit" value="delete">
|
||||
</form>
|
||||
{{ end }}
|
||||
<form class="contents" action="{{ path "/admin/add_podcast_do" }}" method="post">
|
||||
<input class="md:col-start-2 col-span-2" type="text" name="feed" placeholder="rss feed url">
|
||||
<input type="submit" value="add new">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if .User.IsAdmin }}
|
||||
{{ component "block" (props .
|
||||
"Icon" "rss"
|
||||
"Name" "internet radio stations"
|
||||
"Desc" "you can add and update internet radio stations here"
|
||||
) }}
|
||||
<div class="grid grid-cols-[1fr_1fr_min-content_min-content] md:grid-cols-[1fr_1fr_1fr_auto_auto] gap-2 items-center justify-items-end">
|
||||
{{ range $pref := .InternetRadioStations }}
|
||||
<form class="contents" action="{{ printf "/admin/update_internet_radio_station_do?id=%d" $pref.ID | path }}" method="post">
|
||||
<input class="col-span-full md:col-auto" type="text" name="name" value={{ $pref.Name }}>
|
||||
<input type="text" name="streamURL" placeholder="stream url" value={{ $pref.StreamURL }}>
|
||||
<input type="text" name="homepageURL" placeholder="homepage url" value={{ $pref.HomepageURL }}>
|
||||
<input type="submit" value="update">
|
||||
</form>
|
||||
<form class="contents" action="{{ printf "/admin/delete_internet_radio_station_do?id=%d" $pref.ID | path }}" method="post">
|
||||
<input type="submit" value="delete">
|
||||
</form>
|
||||
{{ end }}
|
||||
<form class="contents" action="{{ path "/admin/add_internet_radio_station_do" }}" method="post">
|
||||
<input type="text" name="name" placeholder="name">
|
||||
<input type="text" name="streamURL" placeholder="stream url">
|
||||
<input type="text" name="homepageURL" placeholder="homepage url">
|
||||
<input class="col-auto md:col-span-2" type="submit" value="add">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ component "block" (props .
|
||||
"Icon" "list"
|
||||
"Name" "playlists"
|
||||
"Desc" "choose a local <span class='italic text-gray-800'>.m3u8</span> file containing paths to music files that gonic has scanned. paths should be absolute, prefixed by the <span class='italic text-gray-800'>music-path</span> option that you started gonic with. a playlist will be created from the file and available to subsonic clients"
|
||||
) }}
|
||||
{{ if eq (len .Playlists) 0 }}
|
||||
<span class="text-gray-500">no playlists yet</span>
|
||||
{{ end }}
|
||||
<div class="grid grid-cols-[auto_1fr_auto] md:grid-cols-[auto_repeat(3,min-content)] gap-x-3 gap-y-2 items-center justify-items-end">
|
||||
{{ range $i, $playlist := .Playlists }}
|
||||
<div class="text-right ellipsis">{{ $playlist.Name }}</div>
|
||||
<div class="text-gray-500 whitespace-nowrap">({{ $playlist.TrackCount }} tracks)</div>
|
||||
<div class="text-right text-gray-500 whitespace-nowrap hidden md:block" title="{{ $playlist.CreatedAt }}">{{ $playlist.CreatedAt | dateHuman }}</div>
|
||||
<form class="contents" action="{{ printf "/admin/delete_playlist_do?id=%d" $playlist.ID | path }}" method="post">
|
||||
<input type="submit" value="delete">
|
||||
</form>
|
||||
{{ end }}
|
||||
<form class="col-span-full relative pointer-events-auto" enctype="multipart/form-data" action="{{ path "/admin/upload_playlist_do" }}" method="post">
|
||||
<input class="auto-submit absolute opacity-0" name="playlist-files" type="file" multiple />
|
||||
<input type="button" value="choose m3u8">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
13
server/ctrladmin/adminui/pages/login.tmpl
Normal file
13
server/ctrladmin/adminui/pages/login.tmpl
Normal file
@@ -0,0 +1,13 @@
|
||||
{{ component "layout" . }}
|
||||
{{ component "block" (props .
|
||||
"Icon" "user"
|
||||
"Name" "login"
|
||||
"Desc" "if you are logging in as an admin, the default credentials can be found in the readme"
|
||||
) }}
|
||||
<form class="flex flex-col md:flex-row gap-2 items-end" action="{{ path "/admin/login_do" }}" method="post">
|
||||
<input class="text-center" type="text" id="username" name="username" placeholder="username">
|
||||
<input class="text-center" type="password" id="password" name="password" placeholder="password">
|
||||
<input type="submit" value="login">
|
||||
</form>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
3
server/ctrladmin/adminui/pages/not_found.tmpl
Normal file
3
server/ctrladmin/adminui/pages/not_found.tmpl
Normal file
@@ -0,0 +1,3 @@
|
||||
{{ component "layout" . }}
|
||||
<p class="bg-red-100 p-4 text-center">page not found</p>
|
||||
{{ end }}
|
||||
21
server/ctrladmin/adminui/pages/update_lastfm_api_key.tmpl
Normal file
21
server/ctrladmin/adminui/pages/update_lastfm_api_key.tmpl
Normal file
@@ -0,0 +1,21 @@
|
||||
{{ component "layout" . }}
|
||||
{{ component "layout_user" . }}
|
||||
|
||||
{{ component "block" (props .
|
||||
"Icon" "user"
|
||||
"Name" "update last.fm api keys"
|
||||
"Desc" "you can get an api key from last.fm here <a class='text-blue-500' href='https://www.last.fm/api/account/create' target='_blank' rel='noopener noreferrer'>here</a>. note, only the <span class='italic text-gray-800'>application name</span> field is required"
|
||||
) }}
|
||||
<div class="flex flex-col gap-2 items-end">
|
||||
<p class="text-gray-500">current key <span class="font-bold text-gray-800 italic">{{ default "not set" .CurrentLastFMAPIKey }}</span></p>
|
||||
<p class="text-gray-500">current secret <span class="font-bold text-gray-800 italic">{{ default "not set" .CurrentLastFMAPISecret }}</span></p>
|
||||
<form class="contents" action="{{ path "/admin/update_lastfm_api_key_do" }}" method="post">
|
||||
<input type="text" id="api_key" name="api_key" placeholder="new key">
|
||||
<input type="text" id="secret" name="secret" placeholder="new secret">
|
||||
<input type="submit" value="update">
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 236 KiB |
BIN
server/ctrladmin/adminui/static/inconsolata-v31-latin-500.woff
Normal file
BIN
server/ctrladmin/adminui/static/inconsolata-v31-latin-500.woff
Normal file
Binary file not shown.
BIN
server/ctrladmin/adminui/static/inconsolata-v31-latin-500.woff2
Normal file
BIN
server/ctrladmin/adminui/static/inconsolata-v31-latin-500.woff2
Normal file
Binary file not shown.
BIN
server/ctrladmin/adminui/static/inconsolata-v31-latin-600.woff
Normal file
BIN
server/ctrladmin/adminui/static/inconsolata-v31-latin-600.woff
Normal file
Binary file not shown.
BIN
server/ctrladmin/adminui/static/inconsolata-v31-latin-600.woff2
Normal file
BIN
server/ctrladmin/adminui/static/inconsolata-v31-latin-600.woff2
Normal file
Binary file not shown.
3
server/ctrladmin/adminui/static/main.js
Normal file
3
server/ctrladmin/adminui/static/main.js
Normal file
@@ -0,0 +1,3 @@
|
||||
for (const input of document.querySelectorAll("input.auto-submit, select.auto-submit") || []) {
|
||||
input.onchange = (e) => e.target.form.submit();
|
||||
}
|
||||
1
server/ctrladmin/adminui/static/style.css
Normal file
1
server/ctrladmin/adminui/static/style.css
Normal file
File diff suppressed because one or more lines are too long
52
server/ctrladmin/adminui/style.css
Normal file
52
server/ctrladmin/adminui/style.css
Normal file
@@ -0,0 +1,52 @@
|
||||
@tailwind base;
|
||||
|
||||
form,
|
||||
input,
|
||||
select {
|
||||
all: unset;
|
||||
appearance: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inconsolata";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: local(""),
|
||||
url("/admin/static/inconsolata-v31-latin-500.woff2") format("woff2"),
|
||||
url("/admin/static/inconsolata-v31-latin-500.woff") format("woff");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inconsolata";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local(""),
|
||||
url("/admin/static/inconsolata-v31-latin-600.woff2") format("woff2"),
|
||||
url("/admin/static/inconsolata-v31-latin-600.woff") format("woff");
|
||||
}
|
||||
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
a {
|
||||
@apply text-blue-500;
|
||||
}
|
||||
|
||||
input[type],
|
||||
select {
|
||||
@apply h-6 px-2 leading-[1.5] w-full min-w-[3rem] md:min-w-[8rem] box-border bg-white text-gray-600 shadow-none border-0 outline outline-1 outline-gray-400/50 cursor-pointer overflow-hidden whitespace-nowrap text-ellipsis;
|
||||
}
|
||||
|
||||
input[type="button"],
|
||||
input[type="submit"] {
|
||||
@apply text-center w-[6rem] md:w-[8rem] font-bold;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
@apply max-w-full overflow-hidden text-ellipsis whitespace-nowrap;
|
||||
}
|
||||
13
server/ctrladmin/adminui/tailwind.config.js
Normal file
13
server/ctrladmin/adminui/tailwind.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["*.tmpl", "**/*.tmpl"],
|
||||
theme: {
|
||||
screens: {
|
||||
sm: "100%",
|
||||
md: `870px`,
|
||||
},
|
||||
fontFamily: {
|
||||
mono: ["Inconsolata", "monospace"],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -4,27 +4,27 @@ package ctrladmin
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/sprig"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/fatih/structs"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/oxtoacart/bpool"
|
||||
"github.com/philippta/go-template/html/template"
|
||||
"github.com/sentriz/gormstore"
|
||||
|
||||
"go.senan.xyz/gonic"
|
||||
"go.senan.xyz/gonic/db"
|
||||
"go.senan.xyz/gonic/podcasts"
|
||||
"go.senan.xyz/gonic/server/assets"
|
||||
"go.senan.xyz/gonic/server/ctrladmin/adminui"
|
||||
"go.senan.xyz/gonic/server/ctrlbase"
|
||||
)
|
||||
|
||||
@@ -37,6 +37,10 @@ const (
|
||||
|
||||
func funcMap() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"str": func(in any) string {
|
||||
v, _ := json.Marshal(in)
|
||||
return string(v)
|
||||
},
|
||||
"noCache": func(in string) string {
|
||||
parsed, _ := url.Parse(in)
|
||||
params := parsed.Query()
|
||||
@@ -49,53 +53,49 @@ func funcMap() template.FuncMap {
|
||||
},
|
||||
"dateHuman": humanize.Time,
|
||||
"base64": base64.StdEncoding.EncodeToString,
|
||||
"props": func(parent any, values ...any) map[string]any {
|
||||
if len(values)%2 != 0 {
|
||||
panic("uneven number of key/value pairs")
|
||||
}
|
||||
props := map[string]any{}
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
k, v := fmt.Sprint(values[i]), values[i+1]
|
||||
props[k] = v
|
||||
}
|
||||
merged := map[string]any{}
|
||||
if structs.IsStruct(parent) {
|
||||
merged = structs.Map(parent)
|
||||
}
|
||||
merged["Props"] = props
|
||||
return merged
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
*ctrlbase.Controller
|
||||
buffPool *bpool.BufferPool
|
||||
templates map[string]*template.Template
|
||||
sessDB *gormstore.Store
|
||||
Podcasts *podcasts.Podcasts
|
||||
buffPool *bpool.BufferPool
|
||||
template *template.Template
|
||||
sessDB *gormstore.Store
|
||||
Podcasts *podcasts.Podcasts
|
||||
}
|
||||
|
||||
func New(b *ctrlbase.Controller, sessDB *gormstore.Store, podcasts *podcasts.Podcasts) (*Controller, error) {
|
||||
tmpl := template.
|
||||
tmpl, err := template.
|
||||
New("layout").
|
||||
Funcs(sprig.FuncMap()).
|
||||
Funcs(template.FuncMap(sprig.FuncMap())).
|
||||
Funcs(funcMap()). // static
|
||||
Funcs(template.FuncMap{ // from base
|
||||
"path": b.Path,
|
||||
})
|
||||
|
||||
var err error
|
||||
tmpl, err = tmpl.ParseFS(assets.Partials, "**/*.tmpl")
|
||||
}).
|
||||
ParseFS(adminui.TemplatesFS, "*.tmpl", "**/*.tmpl")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("extend partials: %w", err)
|
||||
return nil, fmt.Errorf("build template: %w", err)
|
||||
}
|
||||
tmpl, err = tmpl.ParseFS(assets.Layouts, "**/*.tmpl")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("extend layouts: %w", err)
|
||||
}
|
||||
|
||||
pagePaths, err := fs.Glob(assets.Pages, "**/*.tmpl")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse pages: %w", err)
|
||||
}
|
||||
pages := map[string]*template.Template{}
|
||||
for _, pagePath := range pagePaths {
|
||||
pageBytes, _ := assets.Pages.ReadFile(pagePath)
|
||||
page, _ := tmpl.Clone()
|
||||
page, _ = page.Parse(string(pageBytes))
|
||||
pageName := filepath.Base(pagePath)
|
||||
pages[pageName] = page
|
||||
}
|
||||
|
||||
return &Controller{
|
||||
Controller: b,
|
||||
buffPool: bpool.NewBufferPool(64),
|
||||
templates: pages,
|
||||
template: tmpl,
|
||||
sessDB: sessDB,
|
||||
Podcasts: podcasts,
|
||||
}, nil
|
||||
@@ -196,12 +196,7 @@ func (c *Controller) H(h handlerAdmin) http.Handler {
|
||||
|
||||
buff := c.buffPool.Get()
|
||||
defer c.buffPool.Put(buff)
|
||||
tmpl, ok := c.templates[resp.template]
|
||||
if !ok {
|
||||
http.Error(w, fmt.Sprintf("finding template %q", resp.template), 500)
|
||||
return
|
||||
}
|
||||
if err := tmpl.Execute(buff, resp.data); err != nil {
|
||||
if err := c.template.ExecuteTemplate(buff, resp.template, resp.data); err != nil {
|
||||
http.Error(w, fmt.Sprintf("executing template: %v", err), 500)
|
||||
return
|
||||
}
|
||||
@@ -236,8 +231,9 @@ type Flash struct {
|
||||
Type FlashType
|
||||
}
|
||||
|
||||
//nolint:gochecknoinits // for now I think it's nice that our types and their
|
||||
// gob registrations are next to each other, in case there's more added later)
|
||||
//
|
||||
//nolint:gochecknoinits // for now I think it's nice that our types and their
|
||||
func init() {
|
||||
gob.Register(&Flash{})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//nolint:goerr113
|
||||
package ctrladmin
|
||||
|
||||
import (
|
||||
@@ -41,6 +42,8 @@ func (c *Controller) ServeLogin(r *http.Request) *Response {
|
||||
}
|
||||
|
||||
func (c *Controller) ServeHome(r *http.Request) *Response {
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
|
||||
data := &templateData{}
|
||||
// stats box
|
||||
c.DB.Model(&db.Artist{}).Count(&data.ArtistCount)
|
||||
@@ -50,8 +53,14 @@ func (c *Controller) ServeHome(r *http.Request) *Response {
|
||||
data.RequestRoot = c.BaseURL(r)
|
||||
data.CurrentLastFMAPIKey, _ = c.DB.GetSetting("lastfm_api_key")
|
||||
data.DefaultListenBrainzURL = listenbrainz.BaseURL
|
||||
|
||||
// users box
|
||||
c.DB.Find(&data.AllUsers)
|
||||
allUsersQ := c.DB.DB
|
||||
if !user.IsAdmin {
|
||||
allUsersQ = allUsersQ.Where("name=?", user.Name)
|
||||
}
|
||||
allUsersQ.Find(&data.AllUsers)
|
||||
|
||||
// recent folders box
|
||||
c.DB.
|
||||
Where("tag_artist_id IS NOT NULL").
|
||||
@@ -64,8 +73,6 @@ func (c *Controller) ServeHome(r *http.Request) *Response {
|
||||
data.LastScanTime = time.Unix(i, 0)
|
||||
}
|
||||
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
|
||||
// playlists box
|
||||
c.DB.
|
||||
Where("user_id=?", user.ID).
|
||||
@@ -91,74 +98,6 @@ func (c *Controller) ServeHome(r *http.Request) *Response {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangeOwnUsername(r *http.Request) *Response {
|
||||
return &Response{template: "change_own_username.tmpl"}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangeOwnUsernameDo(r *http.Request) *Response {
|
||||
username := r.FormValue("username")
|
||||
if err := validateUsername(username); err != nil {
|
||||
return &Response{
|
||||
redirect: r.Referer(),
|
||||
flashW: []string{err.Error()},
|
||||
}
|
||||
}
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
user.Name = username
|
||||
c.DB.Save(user)
|
||||
return &Response{redirect: "/admin/home"}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangeOwnPassword(r *http.Request) *Response {
|
||||
return &Response{template: "change_own_password.tmpl"}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangeOwnPasswordDo(r *http.Request) *Response {
|
||||
passwordOne := r.FormValue("password_one")
|
||||
passwordTwo := r.FormValue("password_two")
|
||||
if err := validatePasswords(passwordOne, passwordTwo); err != nil {
|
||||
return &Response{
|
||||
redirect: r.Referer(),
|
||||
flashW: []string{err.Error()},
|
||||
}
|
||||
}
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
user.Password = passwordOne
|
||||
c.DB.Save(user)
|
||||
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 == "" {
|
||||
@@ -166,11 +105,11 @@ func (c *Controller) ServeLinkLastFMDo(r *http.Request) *Response {
|
||||
}
|
||||
apiKey, err := c.DB.GetSetting("lastfm_api_key")
|
||||
if err != nil {
|
||||
return &Response{code: 500, err: fmt.Sprintf("couldn't get api key: %v", err)}
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("couldn't get api key: %v", err)}}
|
||||
}
|
||||
secret, err := c.DB.GetSetting("lastfm_secret")
|
||||
if err != nil {
|
||||
return &Response{code: 500, err: fmt.Sprintf("couldn't get secret: %v", err)}
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("couldn't get secret: %v", err)}}
|
||||
}
|
||||
sessionKey, err := lastfm.GetSession(apiKey, secret, token)
|
||||
if err != nil {
|
||||
@@ -181,14 +120,18 @@ func (c *Controller) ServeLinkLastFMDo(r *http.Request) *Response {
|
||||
}
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
user.LastFMSession = sessionKey
|
||||
c.DB.Save(&user)
|
||||
if err := c.DB.Save(user).Error; err != nil {
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("save user: %v", err)}}
|
||||
}
|
||||
return &Response{redirect: "/admin/home"}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeUnlinkLastFMDo(r *http.Request) *Response {
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
user.LastFMSession = ""
|
||||
c.DB.Save(&user)
|
||||
if err := c.DB.Save(user).Error; err != nil {
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("save user: %v", err)}}
|
||||
}
|
||||
return &Response{redirect: "/admin/home"}
|
||||
}
|
||||
|
||||
@@ -204,7 +147,9 @@ func (c *Controller) ServeLinkListenBrainzDo(r *http.Request) *Response {
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
user.ListenBrainzURL = url
|
||||
user.ListenBrainzToken = token
|
||||
c.DB.Save(&user)
|
||||
if err := c.DB.Save(user).Error; err != nil {
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("save user: %v", err)}}
|
||||
}
|
||||
return &Response{redirect: "/admin/home"}
|
||||
}
|
||||
|
||||
@@ -212,18 +157,16 @@ func (c *Controller) ServeUnlinkListenBrainzDo(r *http.Request) *Response {
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
user.ListenBrainzURL = ""
|
||||
user.ListenBrainzToken = ""
|
||||
c.DB.Save(&user)
|
||||
if err := c.DB.Save(user).Error; err != nil {
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("save user: %v", err)}}
|
||||
}
|
||||
return &Response{redirect: "/admin/home"}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangeUsername(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"}
|
||||
user, err := selectedUserIfAdmin(c, r)
|
||||
if err != nil {
|
||||
return &Response{code: 400, err: err.Error()}
|
||||
}
|
||||
data := &templateData{}
|
||||
data.SelectedUser = user
|
||||
@@ -234,7 +177,10 @@ func (c *Controller) ServeChangeUsername(r *http.Request) *Response {
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangeUsernameDo(r *http.Request) *Response {
|
||||
username := r.URL.Query().Get("user")
|
||||
user, err := selectedUserIfAdmin(c, r)
|
||||
if err != nil {
|
||||
return &Response{code: 400, err: err.Error()}
|
||||
}
|
||||
usernameNew := r.FormValue("username")
|
||||
if err := validateUsername(usernameNew); err != nil {
|
||||
return &Response{
|
||||
@@ -242,20 +188,17 @@ func (c *Controller) ServeChangeUsernameDo(r *http.Request) *Response {
|
||||
flashW: []string{err.Error()},
|
||||
}
|
||||
}
|
||||
user := c.DB.GetUserByName(username)
|
||||
user.Name = usernameNew
|
||||
c.DB.Save(user)
|
||||
if err := c.DB.Save(user).Error; err != nil {
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("save username: %v", err)}}
|
||||
}
|
||||
return &Response{redirect: "/admin/home"}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangePassword(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"}
|
||||
user, err := selectedUserIfAdmin(c, r)
|
||||
if err != nil {
|
||||
return &Response{code: 400, err: err.Error()}
|
||||
}
|
||||
data := &templateData{}
|
||||
data.SelectedUser = user
|
||||
@@ -266,7 +209,10 @@ func (c *Controller) ServeChangePassword(r *http.Request) *Response {
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangePasswordDo(r *http.Request) *Response {
|
||||
username := r.URL.Query().Get("user")
|
||||
user, err := selectedUserIfAdmin(c, r)
|
||||
if err != nil {
|
||||
return &Response{code: 400, err: err.Error()}
|
||||
}
|
||||
passwordOne := r.FormValue("password_one")
|
||||
passwordTwo := r.FormValue("password_two")
|
||||
if err := validatePasswords(passwordOne, passwordTwo); err != nil {
|
||||
@@ -275,20 +221,17 @@ func (c *Controller) ServeChangePasswordDo(r *http.Request) *Response {
|
||||
flashW: []string{err.Error()},
|
||||
}
|
||||
}
|
||||
user := c.DB.GetUserByName(username)
|
||||
user.Password = passwordOne
|
||||
c.DB.Save(user)
|
||||
if err := c.DB.Save(user).Error; err != nil {
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("save user: %v", err)}}
|
||||
}
|
||||
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"}
|
||||
user, err := selectedUserIfAdmin(c, r)
|
||||
if err != nil {
|
||||
return &Response{code: 400, err: err.Error()}
|
||||
}
|
||||
data := &templateData{}
|
||||
data.SelectedUser = user
|
||||
@@ -299,8 +242,10 @@ func (c *Controller) ServeChangeAvatar(r *http.Request) *Response {
|
||||
}
|
||||
|
||||
func (c *Controller) ServeChangeAvatarDo(r *http.Request) *Response {
|
||||
username := r.URL.Query().Get("user")
|
||||
user := c.DB.GetUserByName(username)
|
||||
user, err := selectedUserIfAdmin(c, r)
|
||||
if err != nil {
|
||||
return &Response{code: 400, err: err.Error()}
|
||||
}
|
||||
avatar, err := getAvatarFile(r)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
@@ -309,26 +254,34 @@ func (c *Controller) ServeChangeAvatarDo(r *http.Request) *Response {
|
||||
}
|
||||
}
|
||||
user.Avatar = avatar
|
||||
c.DB.Save(user)
|
||||
return &Response{redirect: "/admin/home"}
|
||||
if err := c.DB.Save(user).Error; err != nil {
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("save user: %v", err)}}
|
||||
}
|
||||
return &Response{
|
||||
redirect: r.Referer(),
|
||||
flashN: []string{"avatar saved successfully"},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeDeleteAvatarDo(r *http.Request) *Response {
|
||||
username := r.URL.Query().Get("user")
|
||||
user := c.DB.GetUserByName(username)
|
||||
user, err := selectedUserIfAdmin(c, r)
|
||||
if err != nil {
|
||||
return &Response{code: 400, err: err.Error()}
|
||||
}
|
||||
user.Avatar = nil
|
||||
c.DB.Save(user)
|
||||
return &Response{redirect: "/admin/home"}
|
||||
if err := c.DB.Save(user).Error; err != nil {
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("save user: %v", err)}}
|
||||
}
|
||||
return &Response{
|
||||
redirect: r.Referer(),
|
||||
flashN: []string{"avatar deleted successfully"},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) ServeDeleteUser(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"}
|
||||
user, err := selectedUserIfAdmin(c, r)
|
||||
if err != nil {
|
||||
return &Response{code: 400, err: err.Error()}
|
||||
}
|
||||
data := &templateData{}
|
||||
data.SelectedUser = user
|
||||
@@ -339,15 +292,19 @@ func (c *Controller) ServeDeleteUser(r *http.Request) *Response {
|
||||
}
|
||||
|
||||
func (c *Controller) ServeDeleteUserDo(r *http.Request) *Response {
|
||||
username := r.URL.Query().Get("user")
|
||||
user := c.DB.GetUserByName(username)
|
||||
user, err := selectedUserIfAdmin(c, r)
|
||||
if err != nil {
|
||||
return &Response{code: 400, err: err.Error()}
|
||||
}
|
||||
if user.IsAdmin {
|
||||
return &Response{
|
||||
redirect: "/admin/home",
|
||||
flashW: []string{"can't delete the admin user"},
|
||||
}
|
||||
}
|
||||
c.DB.Delete(user)
|
||||
if err := c.DB.Delete(user).Error; err != nil {
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("delete user: %v", err)}}
|
||||
}
|
||||
return &Response{redirect: "/admin/home"}
|
||||
}
|
||||
|
||||
@@ -388,10 +345,10 @@ func (c *Controller) ServeUpdateLastFMAPIKey(r *http.Request) *Response {
|
||||
data := &templateData{}
|
||||
var err error
|
||||
if data.CurrentLastFMAPIKey, err = c.DB.GetSetting("lastfm_api_key"); err != nil {
|
||||
return &Response{code: 500, err: fmt.Sprintf("couldn't get api key: %v", err)}
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("couldn't get api key: %v", err)}}
|
||||
}
|
||||
if data.CurrentLastFMAPISecret, err = c.DB.GetSetting("lastfm_secret"); err != nil {
|
||||
return &Response{code: 500, err: fmt.Sprintf("couldn't get secret: %v", err)}
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("couldn't get secret: %v", err)}}
|
||||
}
|
||||
return &Response{
|
||||
template: "update_lastfm_api_key.tmpl",
|
||||
@@ -409,10 +366,10 @@ func (c *Controller) ServeUpdateLastFMAPIKeyDo(r *http.Request) *Response {
|
||||
}
|
||||
}
|
||||
if err := c.DB.SetSetting("lastfm_api_key", apiKey); err != nil {
|
||||
return &Response{code: 500, err: fmt.Sprintf("couldn't set api key: %v", err)}
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("couldn't set api key: %v", err)}}
|
||||
}
|
||||
if err := c.DB.SetSetting("lastfm_secret", secret); err != nil {
|
||||
return &Response{code: 500, err: fmt.Sprintf("couldn't set secret: %v", err)}
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("couldn't set secret: %v", err)}}
|
||||
}
|
||||
return &Response{redirect: "/admin/home"}
|
||||
}
|
||||
@@ -570,7 +527,7 @@ func (c *Controller) ServeInternetRadioStationAddDo(r *http.Request) *Response {
|
||||
station.Name = name
|
||||
station.HomepageURL = homepageURL
|
||||
if err := c.DB.Save(&station).Error; err != nil {
|
||||
return &Response{code: 500, err: fmt.Sprintf("error saving station: %v", err)}
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("error saving station: %v", err)}}
|
||||
}
|
||||
|
||||
return &Response{
|
||||
@@ -613,7 +570,7 @@ func (c *Controller) ServeInternetRadioStationUpdateDo(r *http.Request) *Respons
|
||||
|
||||
var station db.InternetRadioStation
|
||||
if err := c.DB.Where("id=?", stationID).First(&station).Error; err != nil {
|
||||
return &Response{code: 404, err: fmt.Sprintf("find station by id: %v", err)}
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("find station by id: %v", err)}}
|
||||
}
|
||||
|
||||
station.StreamURL = streamURL
|
||||
@@ -636,11 +593,11 @@ func (c *Controller) ServeInternetRadioStationDeleteDo(r *http.Request) *Respons
|
||||
|
||||
var station db.InternetRadioStation
|
||||
if err := c.DB.Where("id=?", stationID).First(&station).Error; err != nil {
|
||||
return &Response{code: 404, err: fmt.Sprintf("find station by id: %v", err)}
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("find station by id: %v", err)}}
|
||||
}
|
||||
|
||||
if err := c.DB.Where("id=?", stationID).Delete(&db.InternetRadioStation{}).Error; err != nil {
|
||||
return &Response{code: 500, err: fmt.Sprintf("deleting radio station: %v", err)}
|
||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("deleting radio station: %v", err)}}
|
||||
}
|
||||
|
||||
return &Response{
|
||||
@@ -668,3 +625,16 @@ func getAvatarFile(r *http.Request) ([]byte, error) {
|
||||
}
|
||||
return buff.Bytes(), nil
|
||||
}
|
||||
|
||||
func selectedUserIfAdmin(c *Controller, r *http.Request) (*db.User, error) {
|
||||
selectedUsername := r.URL.Query().Get("user")
|
||||
if selectedUsername == "" {
|
||||
return nil, fmt.Errorf("please provide a username")
|
||||
}
|
||||
user := r.Context().Value(CtxUser).(*db.User)
|
||||
if !user.IsAdmin && user.Name != selectedUsername {
|
||||
return nil, fmt.Errorf("must be admin to perform actions for other users")
|
||||
}
|
||||
selectedUser := c.DB.GetUserByName(selectedUsername)
|
||||
return selectedUser, nil
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ import (
|
||||
"go.senan.xyz/gonic/scrobble"
|
||||
"go.senan.xyz/gonic/scrobble/lastfm"
|
||||
"go.senan.xyz/gonic/scrobble/listenbrainz"
|
||||
"go.senan.xyz/gonic/server/assets"
|
||||
"go.senan.xyz/gonic/server/ctrladmin"
|
||||
"go.senan.xyz/gonic/server/ctrladmin/adminui"
|
||||
"go.senan.xyz/gonic/server/ctrlbase"
|
||||
"go.senan.xyz/gonic/server/ctrlsubsonic"
|
||||
"go.senan.xyz/gonic/transcode"
|
||||
@@ -149,7 +149,7 @@ func setupAdmin(r *mux.Router, ctrl *ctrladmin.Controller) {
|
||||
r.Handle("/login", ctrl.H(ctrl.ServeLogin))
|
||||
r.Handle("/login_do", ctrl.HR(ctrl.ServeLoginDo)) // "raw" handler, updates session
|
||||
|
||||
staticHandler := http.StripPrefix("/admin", http.FileServer(http.FS(assets.Static)))
|
||||
staticHandler := http.StripPrefix("/admin", http.FileServer(http.FS(adminui.StaticFS)))
|
||||
r.PathPrefix("/static").Handler(staticHandler)
|
||||
|
||||
// user routes (if session is valid)
|
||||
@@ -157,13 +157,15 @@ func setupAdmin(r *mux.Router, ctrl *ctrladmin.Controller) {
|
||||
routUser.Use(ctrl.WithUserSession)
|
||||
routUser.Handle("/logout", ctrl.HR(ctrl.ServeLogout)) // "raw" handler, updates session
|
||||
routUser.Handle("/home", ctrl.H(ctrl.ServeHome))
|
||||
routUser.Handle("/change_own_username", ctrl.H(ctrl.ServeChangeOwnUsername))
|
||||
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("/change_username", ctrl.H(ctrl.ServeChangeUsername))
|
||||
routUser.Handle("/change_username_do", ctrl.H(ctrl.ServeChangeUsernameDo))
|
||||
routUser.Handle("/change_password", ctrl.H(ctrl.ServeChangePassword))
|
||||
routUser.Handle("/change_password_do", ctrl.H(ctrl.ServeChangePasswordDo))
|
||||
routUser.Handle("/change_avatar", ctrl.H(ctrl.ServeChangeAvatar))
|
||||
routUser.Handle("/change_avatar_do", ctrl.H(ctrl.ServeChangeAvatarDo))
|
||||
routUser.Handle("/delete_avatar_do", ctrl.H(ctrl.ServeDeleteAvatarDo))
|
||||
routUser.Handle("/delete_user", ctrl.H(ctrl.ServeDeleteUser))
|
||||
routUser.Handle("/delete_user_do", ctrl.H(ctrl.ServeDeleteUserDo))
|
||||
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))
|
||||
@@ -176,15 +178,6 @@ func setupAdmin(r *mux.Router, ctrl *ctrladmin.Controller) {
|
||||
// admin routes (if session is valid, and is admin)
|
||||
routAdmin := routUser.NewRoute().Subrouter()
|
||||
routAdmin.Use(ctrl.WithAdminSession)
|
||||
routAdmin.Handle("/change_username", ctrl.H(ctrl.ServeChangeUsername))
|
||||
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))
|
||||
routAdmin.Handle("/create_user_do", ctrl.H(ctrl.ServeCreateUserDo))
|
||||
routAdmin.Handle("/update_lastfm_api_key", ctrl.H(ctrl.ServeUpdateLastFMAPIKey))
|
||||
|
||||
Reference in New Issue
Block a user