256 lines
8 KiB
Go
256 lines
8 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"arimelody-web/admin"
|
|
"arimelody-web/api"
|
|
"arimelody-web/controller"
|
|
"arimelody-web/global"
|
|
"arimelody-web/templates"
|
|
"arimelody-web/view"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
_ "github.com/lib/pq"
|
|
)
|
|
|
|
// used for database migrations
|
|
const DB_VERSION = 1
|
|
|
|
const DEFAULT_PORT int64 = 8080
|
|
|
|
func main() {
|
|
fmt.Printf("made with <3 by ari melody\n\n")
|
|
|
|
// initialise database connection
|
|
if env := os.Getenv("ARIMELODY_DB_HOST"); env != "" { global.Config.DB.Host = env }
|
|
if env := os.Getenv("ARIMELODY_DB_NAME"); env != "" { global.Config.DB.Name = env }
|
|
if env := os.Getenv("ARIMELODY_DB_USER"); env != "" { global.Config.DB.User = env }
|
|
if env := os.Getenv("ARIMELODY_DB_PASS"); env != "" { global.Config.DB.Pass = env }
|
|
if global.Config.DB.Host == "" {
|
|
fmt.Fprintf(os.Stderr, "FATAL: db.host not provided! Exiting...\n")
|
|
os.Exit(1)
|
|
}
|
|
if global.Config.DB.Name == "" {
|
|
fmt.Fprintf(os.Stderr, "FATAL: db.name not provided! Exiting...\n")
|
|
os.Exit(1)
|
|
}
|
|
if global.Config.DB.User == "" {
|
|
fmt.Fprintf(os.Stderr, "FATAL: db.user not provided! Exiting...\n")
|
|
os.Exit(1)
|
|
}
|
|
if global.Config.DB.Pass == "" {
|
|
fmt.Fprintf(os.Stderr, "FATAL: db.pass not provided! Exiting...\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
var err error
|
|
global.DB, err = sqlx.Connect(
|
|
"postgres",
|
|
fmt.Sprintf(
|
|
"host=%s user=%s dbname=%s password='%s' sslmode=disable",
|
|
global.Config.DB.Host,
|
|
global.Config.DB.User,
|
|
global.Config.DB.Name,
|
|
global.Config.DB.Pass,
|
|
),
|
|
)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "FATAL: Unable to initialise database: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
global.DB.SetConnMaxLifetime(time.Minute * 3)
|
|
global.DB.SetMaxOpenConns(10)
|
|
global.DB.SetMaxIdleConns(10)
|
|
defer global.DB.Close()
|
|
|
|
// handle command arguments
|
|
if len(os.Args) > 1 {
|
|
arg := os.Args[1]
|
|
|
|
switch arg {
|
|
case "createInvite":
|
|
fmt.Printf("Creating invite...\n")
|
|
invite, err := controller.CreateInvite(global.DB, 16, time.Hour * 24)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to create invite code: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("Here you go! This code expires in 24 hours: %s\n", invite.Code)
|
|
return
|
|
|
|
case "purgeInvites":
|
|
fmt.Printf("Deleting all invites...\n")
|
|
err := controller.DeleteAllInvites(global.DB)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to delete invites: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("Invites deleted successfully.\n")
|
|
return
|
|
|
|
case "listAccounts":
|
|
accounts, err := controller.GetAllAccounts(global.DB)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to fetch accounts: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
for _, account := range accounts {
|
|
fmt.Printf(
|
|
"User: %s\n" +
|
|
"\tID: %s\n" +
|
|
"\tEmail: %s\n" +
|
|
"\tCreated: %s\n",
|
|
account.Username,
|
|
account.ID,
|
|
account.Email,
|
|
account.CreatedAt,
|
|
)
|
|
}
|
|
return
|
|
|
|
case "deleteAccount":
|
|
if len(os.Args) < 2 {
|
|
fmt.Fprintf(os.Stderr, "FATAL: Account name not specified for -deleteAccount\n")
|
|
os.Exit(1)
|
|
}
|
|
username := os.Args[2]
|
|
fmt.Printf("Deleting account \"%s\"...\n", username)
|
|
|
|
account, err := controller.GetAccount(global.DB, username)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to fetch account \"%s\": %v\n", username, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if account == nil {
|
|
fmt.Fprintf(os.Stderr, "Account \"%s\" does not exist.\n", username)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("You are about to delete \"%s\". Are you sure? (y/[N]): ", account.Username)
|
|
res := ""
|
|
fmt.Scanln(&res)
|
|
if !strings.HasPrefix(res, "y") {
|
|
return
|
|
}
|
|
|
|
err = controller.DeleteAccount(global.DB, username)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Failed to delete account: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("Account \"%s\" deleted successfully.\n", account.Username)
|
|
return
|
|
|
|
}
|
|
|
|
// command help
|
|
fmt.Print(
|
|
"Available commands:\n\n" +
|
|
"createInvite:\n\tCreates an invite code to register new accounts.\n" +
|
|
"purgeInvites:\n\tDeletes all available invite codes.\n" +
|
|
"listAccounts:\n\tLists all active accounts.\n",
|
|
"deleteAccount <username>:\n\tDeletes an account with a given `username`.\n",
|
|
)
|
|
return
|
|
}
|
|
|
|
// handle DB migrations
|
|
controller.CheckDBVersionAndMigrate(global.DB)
|
|
|
|
// initial invite code
|
|
accountsCount := 0
|
|
err = global.DB.Get(&accountsCount, "SELECT count(*) FROM account")
|
|
if err != nil { panic(err) }
|
|
if accountsCount == 0 {
|
|
_, err := global.DB.Exec("DELETE FROM invite")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to clear existing invite codes: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
invite, err := controller.CreateInvite(global.DB, 16, time.Hour * 24)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite code: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Fprintf(os.Stdout, "No accounts exist! Generated invite code: " + string(invite.Code) + "\nUse this at %s/admin/register.\n", global.Config.BaseUrl)
|
|
}
|
|
|
|
// delete expired invites
|
|
err = controller.DeleteExpiredInvites(global.DB)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to clear expired invite codes: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// start the web server!
|
|
mux := createServeMux()
|
|
fmt.Printf("Now serving at http://127.0.0.1:%d\n", global.Config.Port)
|
|
log.Fatal(
|
|
http.ListenAndServe(fmt.Sprintf(":%d", global.Config.Port),
|
|
global.HTTPLog(global.DefaultHeaders(mux)),
|
|
))
|
|
}
|
|
|
|
func createServeMux() *http.ServeMux {
|
|
mux := http.NewServeMux()
|
|
|
|
mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler()))
|
|
mux.Handle("/api/", http.StripPrefix("/api", api.Handler()))
|
|
mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler()))
|
|
mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler(filepath.Join(global.Config.DataDirectory, "uploads"))))
|
|
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodHead {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
|
|
err := templates.Pages["index"].Execute(w, nil)
|
|
if err != nil {
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
staticHandler("public").ServeHTTP(w, r)
|
|
}))
|
|
|
|
return mux
|
|
}
|
|
|
|
func staticHandler(directory string) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path)))
|
|
|
|
// does the file exist?
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
}
|
|
|
|
// is thjs a directory? (forbidden)
|
|
if info.IsDir() {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
http.FileServer(http.Dir(directory)).ServeHTTP(w, r)
|
|
})
|
|
}
|