merged main, dev, and i guess got accounts working??
i am so good at commit messages :3
This commit is contained in:
commit
5566a795da
53 changed files with 1366 additions and 398 deletions
|
@ -2,23 +2,22 @@ package admin
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"arimelody-web/controller"
|
||||
"arimelody-web/global"
|
||||
"arimelody-web/model"
|
||||
)
|
||||
|
||||
type (
|
||||
Session struct {
|
||||
Token string
|
||||
UserID string
|
||||
Account *model.Account
|
||||
Expires time.Time
|
||||
}
|
||||
)
|
||||
|
||||
const TOKEN_LENGTH = 64
|
||||
const TOKEN_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
var ADMIN_BYPASS = func() bool {
|
||||
if global.Args["adminBypass"] == "true" {
|
||||
|
@ -28,24 +27,12 @@ var ADMIN_BYPASS = func() bool {
|
|||
return false
|
||||
}()
|
||||
|
||||
var ADMIN_ID_DISCORD = os.Getenv("DISCORD_ADMIN")
|
||||
|
||||
var sessions []*Session
|
||||
|
||||
func createSession(username string, expires time.Time) Session {
|
||||
func createSession(account *model.Account, expires time.Time) Session {
|
||||
return Session{
|
||||
Token: string(generateToken()),
|
||||
UserID: username,
|
||||
Token: string(controller.GenerateAlnumString(TOKEN_LENGTH)),
|
||||
Account: account,
|
||||
Expires: expires,
|
||||
}
|
||||
}
|
||||
|
||||
func generateToken() string {
|
||||
var token []byte
|
||||
|
||||
for i := 0; i < TOKEN_LENGTH; i++ {
|
||||
token = append(token, TOKEN_CHARS[rand.Intn(len(TOKEN_CHARS))])
|
||||
}
|
||||
|
||||
return string(token)
|
||||
}
|
||||
|
|
|
@ -32,12 +32,19 @@ func serveArtist() http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
type Artist struct {
|
||||
*model.Artist
|
||||
Credits []*model.Credit
|
||||
type ArtistResponse struct {
|
||||
Account *model.Account
|
||||
Artist *model.Artist
|
||||
Credits []*model.Credit
|
||||
}
|
||||
|
||||
err = pages["artist"].Execute(w, Artist{ Artist: artist, Credits: credits })
|
||||
account := r.Context().Value("account").(*model.Account)
|
||||
|
||||
err = pages["artist"].Execute(w, ArtistResponse{
|
||||
Account: account,
|
||||
Artist: artist,
|
||||
Credits: credits,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Error rendering admin track page for %s: %s\n", id, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
|
|
@ -52,7 +52,6 @@
|
|||
makeMagicList(creditList, ".credit");
|
||||
|
||||
function rigCredit(el) {
|
||||
console.log(el);
|
||||
const artistID = el.dataset.artist;
|
||||
const deleteBtn = el.querySelector("a.delete");
|
||||
|
||||
|
|
|
@ -12,12 +12,12 @@
|
|||
|
||||
<form action="/api/v1/music/{{.ID}}/tracks">
|
||||
<ul>
|
||||
{{range .Tracks}}
|
||||
<li class="track" data-track="{{.ID}}" data-title="{{.Title}}" data-number="{{.Number}}" draggable="true">
|
||||
{{range $i, $track := .Tracks}}
|
||||
<li class="track" data-track="{{$track.ID}}" data-title="{{$track.Title}}" data-number="{{$track.Add $i 1}}" draggable="true">
|
||||
<div>
|
||||
<p class="track-name">
|
||||
<span class="track-number">{{.Number}}</span>
|
||||
{{.Title}}
|
||||
<span class="track-number">{{$track.Add $i 1}}</span>
|
||||
{{$track.Title}}
|
||||
</p>
|
||||
<a class="delete">Delete</a>
|
||||
</div>
|
||||
|
|
240
admin/http.go
240
admin/http.go
|
@ -9,14 +9,16 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"arimelody-web/discord"
|
||||
"arimelody-web/global"
|
||||
"arimelody-web/controller"
|
||||
"arimelody-web/global"
|
||||
"arimelody-web/model"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type loginData struct {
|
||||
DiscordURI string
|
||||
type TemplateData struct {
|
||||
Account *model.Account
|
||||
Token string
|
||||
}
|
||||
|
||||
|
@ -25,57 +27,62 @@ func Handler() http.Handler {
|
|||
|
||||
mux.Handle("/login", LoginHandler())
|
||||
mux.Handle("/create-account", createAccountHandler())
|
||||
mux.Handle("/logout", MustAuthorise(LogoutHandler()))
|
||||
mux.Handle("/logout", RequireAccount(global.DB, LogoutHandler()))
|
||||
mux.Handle("/static/", http.StripPrefix("/static", staticHandler()))
|
||||
mux.Handle("/release/", MustAuthorise(http.StripPrefix("/release", serveRelease())))
|
||||
mux.Handle("/artist/", MustAuthorise(http.StripPrefix("/artist", serveArtist())))
|
||||
mux.Handle("/track/", MustAuthorise(http.StripPrefix("/track", serveTrack())))
|
||||
mux.Handle("/release/", RequireAccount(global.DB, http.StripPrefix("/release", serveRelease())))
|
||||
mux.Handle("/artist/", RequireAccount(global.DB, http.StripPrefix("/artist", serveArtist())))
|
||||
mux.Handle("/track/", RequireAccount(global.DB, http.StripPrefix("/track", serveTrack())))
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
session := GetSession(r)
|
||||
if session == nil {
|
||||
account, err := controller.GetAccountByRequest(global.DB, r)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %s\n", err)
|
||||
}
|
||||
if account == nil {
|
||||
http.Redirect(w, r, "/admin/login", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
releases, err := controller.GetAllReleases(global.DB, false, 0, true)
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: Failed to pull releases: %s\n", err)
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
artists, err := controller.GetAllArtists(global.DB)
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: Failed to pull artists: %s\n", err)
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to pull artists: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tracks, err := controller.GetOrphanTracks(global.DB)
|
||||
if err != nil {
|
||||
fmt.Printf("FATAL: Failed to pull orphan tracks: %s\n", err)
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to pull orphan tracks: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
type IndexData struct {
|
||||
Account *model.Account
|
||||
Releases []*model.Release
|
||||
Artists []*model.Artist
|
||||
Tracks []*model.Track
|
||||
}
|
||||
|
||||
err = pages["index"].Execute(w, IndexData{
|
||||
Account: account,
|
||||
Releases: releases,
|
||||
Artists: artists,
|
||||
Tracks: tracks,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Error executing template: %s\n", err)
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to render admin index: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
@ -84,125 +91,109 @@ func Handler() http.Handler {
|
|||
return mux
|
||||
}
|
||||
|
||||
func MustAuthorise(next http.Handler) http.Handler {
|
||||
func RequireAccount(db *sqlx.DB, next http.Handler) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
session := GetSession(r)
|
||||
if session == nil {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
account, err := controller.GetAccountByRequest(db, r)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %s\n", err.Error())
|
||||
return
|
||||
}
|
||||
if account == nil {
|
||||
// TODO: include context in redirect
|
||||
http.Redirect(w, r, "/admin/login", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), "session", session)
|
||||
ctx := context.WithValue(r.Context(), "account", account)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func GetSession(r *http.Request) *Session {
|
||||
if ADMIN_BYPASS {
|
||||
return &Session{}
|
||||
}
|
||||
|
||||
var token = ""
|
||||
// is the session token in context?
|
||||
var ctx_session = r.Context().Value("session")
|
||||
if ctx_session != nil {
|
||||
token = ctx_session.(*Session).Token
|
||||
}
|
||||
|
||||
// okay, is it in the auth header?
|
||||
if token == "" {
|
||||
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
|
||||
token = r.Header.Get("Authorization")[7:]
|
||||
}
|
||||
}
|
||||
// finally, is it in the cookie?
|
||||
if token == "" {
|
||||
cookie, err := r.Cookie("token")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
token = cookie.Value
|
||||
}
|
||||
|
||||
var session *Session = nil
|
||||
for _, s := range sessions {
|
||||
if s.Expires.Before(time.Now()) {
|
||||
// expired session. remove it from the list!
|
||||
new_sessions := []*Session{}
|
||||
for _, ns := range sessions {
|
||||
if ns.Token == s.Token {
|
||||
continue
|
||||
}
|
||||
new_sessions = append(new_sessions, ns)
|
||||
}
|
||||
sessions = new_sessions
|
||||
continue
|
||||
}
|
||||
|
||||
if s.Token == token {
|
||||
session = s
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
func LoginHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// if !discord.CREDENTIALS_PROVIDED || ADMIN_ID_DISCORD == "" {
|
||||
// http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// fmt.Println(discord.CLIENT_ID)
|
||||
// fmt.Println(discord.API_ENDPOINT)
|
||||
// fmt.Println(discord.REDIRECT_URI)
|
||||
if r.Method == http.MethodGet {
|
||||
account, err := controller.GetAccountByRequest(global.DB, r)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account: %s\n", err.Error())
|
||||
return
|
||||
}
|
||||
if account != nil {
|
||||
http.Redirect(w, r, "/admin", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
|
||||
if code == "" {
|
||||
pages["login"].Execute(w, loginData{DiscordURI: discord.REDIRECT_URI})
|
||||
err = pages["login"].Execute(w, TemplateData{})
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARN: Error rendering admin login page: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
auth_token, err := discord.GetOAuthTokenFromCode(code)
|
||||
if r.Method != http.MethodPost {
|
||||
http.NotFound(w, r);
|
||||
return
|
||||
}
|
||||
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to retrieve discord access token: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
fmt.Fprintf(os.Stderr, "WARN: Error logging in: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
discord_user, err := discord.GetDiscordUserFromAuth(auth_token)
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
TOTP string `json:"totp"`
|
||||
}
|
||||
data := LoginRequest{
|
||||
Username: r.Form.Get("username"),
|
||||
Password: r.Form.Get("password"),
|
||||
TOTP: r.Form.Get("totp"),
|
||||
}
|
||||
|
||||
account, err := controller.GetAccount(global.DB, data.Username)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to retrieve discord user information: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
http.Error(w, "No account exists with this username and password.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if discord_user.ID != ADMIN_ID_DISCORD {
|
||||
// TODO: unauthorized user; revoke the token
|
||||
fmt.Printf("Unauthorized login attempted: %s\n", discord_user.ID)
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
err = bcrypt.CompareHashAndPassword(account.Password, []byte(data.Password))
|
||||
if err != nil {
|
||||
http.Error(w, "No account exists with this username and password.", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: check TOTP
|
||||
|
||||
// login success!
|
||||
session := createSession(discord_user.Username, time.Now().Add(24 * time.Hour))
|
||||
sessions = append(sessions, &session)
|
||||
token, err := controller.CreateToken(global.DB, account.ID, r.UserAgent())
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to create token: %s\n", err.Error())
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cookie := http.Cookie{}
|
||||
cookie.Name = "token"
|
||||
cookie.Value = session.Token
|
||||
cookie.Expires = time.Now().Add(24 * time.Hour)
|
||||
if strings.HasPrefix(global.HTTP_DOMAIN, "https") {
|
||||
cookie.Name = global.COOKIE_TOKEN
|
||||
cookie.Value = token.Token
|
||||
cookie.Expires = token.ExpiresAt
|
||||
if strings.HasPrefix(global.Config.BaseUrl, "https") {
|
||||
cookie.Secure = true
|
||||
}
|
||||
cookie.HttpOnly = true
|
||||
cookie.Path = "/"
|
||||
http.SetCookie(w, &cookie)
|
||||
|
||||
err = pages["login"].Execute(w, loginData{Token: session.Token})
|
||||
err = pages["login"].Execute(w, TemplateData{
|
||||
Account: account,
|
||||
Token: token.Token,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Error rendering admin login page: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
@ -218,33 +209,48 @@ func LogoutHandler() http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
session := GetSession(r)
|
||||
token_str := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||
|
||||
// remove this session from the list
|
||||
sessions = func (token string) []*Session {
|
||||
new_sessions := []*Session{}
|
||||
for _, session := range sessions {
|
||||
if session.Token != token {
|
||||
new_sessions = append(new_sessions, session)
|
||||
}
|
||||
if token_str == "" {
|
||||
cookie, err := r.Cookie(global.COOKIE_TOKEN)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARN: Error fetching token cookie: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if cookie != nil {
|
||||
token_str = cookie.Value
|
||||
}
|
||||
return new_sessions
|
||||
}(session.Token)
|
||||
|
||||
err := pages["logout"].Execute(w, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("Error rendering admin logout page: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(token_str) > 0 {
|
||||
err := controller.DeleteToken(global.DB, token_str)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "WARN: Failed to revoke token: %s\n", err.Error())
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cookie := http.Cookie{}
|
||||
cookie.Name = global.COOKIE_TOKEN
|
||||
cookie.Value = ""
|
||||
cookie.Expires = time.Now()
|
||||
if strings.HasPrefix(global.Config.BaseUrl, "https") {
|
||||
cookie.Secure = true
|
||||
}
|
||||
cookie.HttpOnly = true
|
||||
cookie.Path = "/"
|
||||
http.SetCookie(w, &cookie)
|
||||
http.Redirect(w, r, "/admin/login", http.StatusFound)
|
||||
})
|
||||
}
|
||||
|
||||
func createAccountHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
err := pages["create-account"].Execute(w, nil)
|
||||
err := pages["create-account"].Execute(w, TemplateData{})
|
||||
if err != nil {
|
||||
fmt.Printf("Error rendering admin crearte account page: %s\n", err)
|
||||
fmt.Printf("Error rendering create account page: %s\n", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ func serveRelease() http.Handler {
|
|||
slices := strings.Split(r.URL.Path[1:], "/")
|
||||
releaseID := slices[0]
|
||||
|
||||
account := r.Context().Value("account").(*model.Account)
|
||||
|
||||
release, err := controller.GetRelease(global.DB, releaseID, true)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
|
@ -26,12 +28,6 @@ func serveRelease() http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
authorised := GetSession(r) != nil
|
||||
if !authorised && !release.Visible {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if len(slices) > 1 {
|
||||
switch slices[1] {
|
||||
case "editcredits":
|
||||
|
@ -60,7 +56,15 @@ func serveRelease() http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
err = pages["release"].Execute(w, release)
|
||||
type ReleaseResponse struct {
|
||||
Account *model.Account
|
||||
Release *model.Release
|
||||
}
|
||||
|
||||
err = pages["release"].Execute(w, ReleaseResponse{
|
||||
Account: account,
|
||||
Release: release,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Error rendering admin release page for %s: %s\n", release.ID, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
|
|
@ -43,7 +43,7 @@ nav .title {
|
|||
|
||||
color: inherit;
|
||||
}
|
||||
nav a {
|
||||
.nav-item {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
|
||||
|
@ -53,16 +53,17 @@ nav a {
|
|||
display: flex;
|
||||
|
||||
line-height: 2em;
|
||||
text-decoration: none;
|
||||
|
||||
color: inherit;
|
||||
}
|
||||
nav a:hover {
|
||||
.nav-item:hover {
|
||||
background: #00000010;
|
||||
text-decoration: none;
|
||||
}
|
||||
nav a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
nav #logout {
|
||||
margin-left: auto;
|
||||
/* margin-left: auto; */
|
||||
}
|
||||
|
||||
main {
|
||||
|
@ -114,6 +115,10 @@ a img.icon {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.flex-fill {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 520px) {
|
||||
body {
|
||||
font-size: 12px;
|
||||
|
|
|
@ -32,12 +32,19 @@ func serveTrack() http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
type Track struct {
|
||||
*model.Track
|
||||
type TrackResponse struct {
|
||||
Account *model.Account
|
||||
Track *model.Track
|
||||
Releases []*model.Release
|
||||
}
|
||||
|
||||
err = pages["track"].Execute(w, Track{ Track: track, Releases: releases })
|
||||
account := r.Context().Value("account").(*model.Account)
|
||||
|
||||
err = pages["track"].Execute(w, TrackResponse{
|
||||
Account: account,
|
||||
Track: track,
|
||||
Releases: releases,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Error rendering admin track page for %s: %s\n", id, err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{{define "head"}}
|
||||
<title>Editing {{.Name}} - ari melody 💫</title>
|
||||
<title>Editing {{.Artist.Name}} - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="{{.Artist.GetAvatar}}" type="image/x-icon">
|
||||
|
||||
<link rel="stylesheet" href="/admin/static/edit-artist.css">
|
||||
{{end}}
|
||||
|
@ -8,20 +9,20 @@
|
|||
<main>
|
||||
<h1>Editing Artist</h1>
|
||||
|
||||
<div id="artist" data-id="{{.ID}}">
|
||||
<div id="artist" data-id="{{.Artist.ID}}">
|
||||
<div class="artist-avatar">
|
||||
<img src="{{.Avatar}}" alt="" width="256" loading="lazy" id="avatar">
|
||||
<img src="{{.Artist.Avatar}}" alt="" width="256" loading="lazy" id="avatar">
|
||||
<input type="file" id="avatar-file" name="Artwork" accept=".png,.jpg,.jpeg" hidden>
|
||||
<button id="remove-avatar">Remove</button>
|
||||
</div>
|
||||
<div class="artist-info">
|
||||
<p class="attribute-header">Name</p>
|
||||
<h2 class="artist-name">
|
||||
<input type="text" id="name" name="artist-name" value="{{.Name}}">
|
||||
<input type="text" id="name" name="artist-name" value="{{.Artist.Name}}">
|
||||
</h2>
|
||||
|
||||
<p class="attribute-header">Website</p>
|
||||
<input type="text" id="website" name="website" value="{{.Website}}">
|
||||
<input type="text" id="website" name="website" value="{{.Artist.Website}}">
|
||||
|
||||
<div class="artist-actions">
|
||||
<button type="submit" class="save" id="save" disabled>Save</button>
|
||||
|
@ -36,13 +37,13 @@
|
|||
{{if .Credits}}
|
||||
{{range .Credits}}
|
||||
<div class="credit">
|
||||
<img src="{{.Release.Artwork}}" alt="" width="64" loading="lazy" class="release-artwork">
|
||||
<img src="{{.Artist.Release.Artwork}}" alt="" width="64" loading="lazy" class="release-artwork">
|
||||
<div class="credit-info">
|
||||
<h3 class="credit-name"><a href="/admin/release/{{.Release.ID}}">{{.Release.Title}}</a></h3>
|
||||
<p class="credit-artists">{{.Release.PrintArtists true true}}</p>
|
||||
<h3 class="credit-name"><a href="/admin/release/{{.Artist.Release.ID}}">{{.Artist.Release.Title}}</a></h3>
|
||||
<p class="credit-artists">{{.Artist.Release.PrintArtists true true}}</p>
|
||||
<p class="artist-role">
|
||||
Role: {{.Role}}
|
||||
{{if .Primary}}
|
||||
Role: {{.Artist.Role}}
|
||||
{{if .Artist.Primary}}
|
||||
<small>(Primary)</small>
|
||||
{{end}}
|
||||
</p>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{{define "head"}}
|
||||
<title>Editing {{.Title}} - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="{{.GetArtwork}}" type="image/x-icon">
|
||||
<title>Editing {{.Release.Title}} - ari melody 💫</title>
|
||||
<link rel="shortcut icon" href="{{.Release.GetArtwork}}" type="image/x-icon">
|
||||
|
||||
<link rel="stylesheet" href="/admin/static/edit-release.css">
|
||||
{{end}}
|
||||
|
@ -8,21 +8,21 @@
|
|||
{{define "content"}}
|
||||
<main>
|
||||
|
||||
<div id="release" data-id="{{.ID}}">
|
||||
<div id="release" data-id="{{.Release.ID}}">
|
||||
<div class="release-artwork">
|
||||
<img src="{{.Artwork}}" alt="" width="256" loading="lazy" id="artwork">
|
||||
<img src="{{.Release.Artwork}}" alt="" width="256" loading="lazy" id="artwork">
|
||||
<input type="file" id="artwork-file" name="Artwork" accept=".png,.jpg,.jpeg" hidden>
|
||||
<button id="remove-artwork">Remove</button>
|
||||
</div>
|
||||
<div class="release-info">
|
||||
<h1 class="release-title">
|
||||
<input type="text" id="title" name="Title" value="{{.Title}}" autocomplete="on">
|
||||
<input type="text" id="title" name="Title" value="{{.Release.Title}}" autocomplete="on">
|
||||
</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>
|
||||
{{$t := .ReleaseType}}
|
||||
{{$t := .Release.ReleaseType}}
|
||||
<select name="Type" id="type">
|
||||
<option value="single" {{if eq $t "single"}}selected{{end}}>
|
||||
Single
|
||||
|
@ -44,71 +44,71 @@
|
|||
<td>
|
||||
<textarea
|
||||
name="Description"
|
||||
value="{{.Description}}"
|
||||
value="{{.Release.Description}}"
|
||||
placeholder="No description provided."
|
||||
rows="3"
|
||||
id="description"
|
||||
>{{.Description}}</textarea>
|
||||
>{{.Release.Description}}</textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Release Date</td>
|
||||
<td>
|
||||
<input type="datetime-local" name="release-date" id="release-date" value="{{.TextReleaseDate}}">
|
||||
<input type="datetime-local" name="release-date" id="release-date" value="{{.Release.TextReleaseDate}}">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Buy Name</td>
|
||||
<td>
|
||||
<input type="text" name="buyname" id="buyname" value="{{.Buyname}}" autocomplete="on">
|
||||
<input type="text" name="buyname" id="buyname" value="{{.Release.Buyname}}" autocomplete="on">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Buy Link</td>
|
||||
<td>
|
||||
<input type="text" name="buylink" id="buylink" value="{{.Buylink}}" autocomplete="on">
|
||||
<input type="text" name="buylink" id="buylink" value="{{.Release.Buylink}}" autocomplete="on">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Copyright</td>
|
||||
<td>
|
||||
<input type="text" name="copyright" id="copyright" value="{{.Copyright}}" autocomplete="on">
|
||||
<input type="text" name="copyright" id="copyright" value="{{.Release.Copyright}}" autocomplete="on">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Copyright URL</td>
|
||||
<td>
|
||||
<input type="text" name="copyright-url" id="copyright-url" value="{{.CopyrightURL}}" autocomplete="on">
|
||||
<input type="text" name="copyright-url" id="copyright-url" value="{{.Release.CopyrightURL}}" autocomplete="on">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Visible</td>
|
||||
<td>
|
||||
<select name="Visibility" id="visibility">
|
||||
<option value="true" {{if .Visible}}selected{{end}}>True</option>
|
||||
<option value="false" {{if not .Visible}}selected{{end}}>False</option>
|
||||
<option value="true" {{if .Release.Visible}}selected{{end}}>True</option>
|
||||
<option value="false" {{if not .Release.Visible}}selected{{end}}>False</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="release-actions">
|
||||
<a href="/music/{{.ID}}" class="button" target="_blank">Gateway <img class="icon" src="/img/external-link.svg"/></a>
|
||||
<a href="/music/{{.Release.ID}}" class="button" target="_blank">Gateway <img class="icon" src="/img/external-link.svg"/></a>
|
||||
<button type="submit" class="save" id="save" disabled>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-title">
|
||||
<h2>Credits ({{len .Credits}})</h2>
|
||||
<h2>Credits ({{len .Release.Credits}})</h2>
|
||||
<a class="button edit"
|
||||
href="/admin/release/{{.ID}}/editcredits"
|
||||
hx-get="/admin/release/{{.ID}}/editcredits"
|
||||
href="/admin/release/{{.Release.ID}}/editcredits"
|
||||
hx-get="/admin/release/{{.Release.ID}}/editcredits"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
>Edit</a>
|
||||
</div>
|
||||
<div class="card credits">
|
||||
{{range .Credits}}
|
||||
{{range .Release.Credits}}
|
||||
<div class="credit">
|
||||
<img src="{{.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
|
||||
<div class="credit-info">
|
||||
|
@ -122,37 +122,37 @@
|
|||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if not .Credits}}
|
||||
{{if not .Release.Credits}}
|
||||
<p>There are no credits.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card-title">
|
||||
<h2>Links ({{len .Links}})</h2>
|
||||
<h2>Links ({{len .Release.Links}})</h2>
|
||||
<a class="button edit"
|
||||
href="/admin/release/{{.ID}}/editlinks"
|
||||
hx-get="/admin/release/{{.ID}}/editlinks"
|
||||
href="/admin/release/{{.Release.ID}}/editlinks"
|
||||
hx-get="/admin/release/{{.Release.ID}}/editlinks"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
>Edit</a>
|
||||
</div>
|
||||
<div class="card links">
|
||||
{{range .Links}}
|
||||
{{range .Release.Links}}
|
||||
<a href="{{.URL}}" target="_blank" class="button" data-name="{{.Name}}">{{.Name}} <img class="icon" src="/img/external-link.svg"/></a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="card-title" id="tracks">
|
||||
<h2>Tracklist ({{len .Tracks}})</h2>
|
||||
<h2>Tracklist ({{len .Release.Tracks}})</h2>
|
||||
<a class="button edit"
|
||||
href="/admin/release/{{.ID}}/edittracks"
|
||||
hx-get="/admin/release/{{.ID}}/edittracks"
|
||||
href="/admin/release/{{.Release.ID}}/edittracks"
|
||||
hx-get="/admin/release/{{.Release.ID}}/edittracks"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
>Edit</a>
|
||||
</div>
|
||||
<div class="card tracks">
|
||||
{{range $i, $track := .Tracks}}
|
||||
{{range $i, $track := .Release.Tracks}}
|
||||
<div class="track" data-id="{{$track.ID}}">
|
||||
<h2 class="track-title">
|
||||
<span class="track-number">{{.Add $i 1}}</span>
|
||||
|
|
|
@ -8,30 +8,30 @@
|
|||
<main>
|
||||
<h1>Editing Track</h1>
|
||||
|
||||
<div id="track" data-id="{{.ID}}">
|
||||
<div id="track" data-id="{{.Track.ID}}">
|
||||
<div class="track-info">
|
||||
<p class="attribute-header">Title</p>
|
||||
<h2 class="track-title">
|
||||
<input type="text" id="title" name="Title" value="{{.Title}}">
|
||||
<input type="text" id="title" name="Title" value="{{.Track.Title}}">
|
||||
</h2>
|
||||
|
||||
<p class="attribute-header">Description</p>
|
||||
<textarea
|
||||
name="Description"
|
||||
value="{{.Description}}"
|
||||
value="{{.Track.Description}}"
|
||||
placeholder="No description provided."
|
||||
rows="5"
|
||||
id="description"
|
||||
>{{.Description}}</textarea>
|
||||
>{{.Track.Description}}</textarea>
|
||||
|
||||
<p class="attribute-header">Lyrics</p>
|
||||
<textarea
|
||||
name="Lyrics"
|
||||
value="{{.Lyrics}}"
|
||||
value="{{.Track.Lyrics}}"
|
||||
placeholder="There are no lyrics."
|
||||
rows="5"
|
||||
id="lyrics"
|
||||
>{{.Lyrics}}</textarea>
|
||||
>{{.Track.Lyrics}}</textarea>
|
||||
|
||||
<div class="track-actions">
|
||||
<button type="submit" class="save" id="save" disabled>Save</button>
|
||||
|
|
|
@ -17,9 +17,18 @@
|
|||
<header>
|
||||
<nav>
|
||||
<img src="/img/favicon.png" alt="" class="icon">
|
||||
<a href="/">arimelody.me</a>
|
||||
<a href="/admin">home</a>
|
||||
<a href="/admin/logout" id="logout">log out</a>
|
||||
<div class="nav-item">
|
||||
<a href="/">arimelody.me</a>
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<a href="/admin">home</a>
|
||||
</div>
|
||||
<div class="flex-fill"></div>
|
||||
{{if .Account}}
|
||||
<div class="nav-item">
|
||||
<a href="/admin/logout" id="logout">logged in as {{.Account.Username}}. log out</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
|
|
|
@ -72,16 +72,14 @@ button:active {
|
|||
<main>
|
||||
{{if .Token}}
|
||||
|
||||
<meta http-equiv="refresh" content="5;url=/admin/" />
|
||||
<meta http-equiv="refresh" content="0;url=/admin/" />
|
||||
<p>
|
||||
Logged in successfully.
|
||||
You should be redirected to <a href="/admin">/admin</a> in 5 seconds.
|
||||
You should be redirected to <a href="/admin">/admin</a> soon.
|
||||
</p>
|
||||
|
||||
{{else}}
|
||||
|
||||
<!-- <p>Log in with <a href="{{.DiscordURI}}" class="discord">Discord</a>.</p> -->
|
||||
|
||||
<form action="/admin/login" method="POST" id="login">
|
||||
<div>
|
||||
<label for="username">Username</label>
|
||||
|
@ -90,8 +88,8 @@ button:active {
|
|||
<label for="password">Password</label>
|
||||
<input type="password" name="password" value="">
|
||||
|
||||
<label for="code">Code</label>
|
||||
<input type="text" name="code" value="">
|
||||
<label for="totp">TOTP</label>
|
||||
<input type="text" name="totp" value="">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="save">Login</button>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue