2024-03-18 10:34:43 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2024-09-03 07:07:45 +00:00
|
|
|
"errors"
|
2024-04-16 21:58:39 +00:00
|
|
|
"fmt"
|
2025-02-07 12:41:25 +00:00
|
|
|
stdLog "log"
|
2025-01-23 09:39:40 +00:00
|
|
|
"math"
|
2025-01-21 14:53:18 +00:00
|
|
|
"math/rand"
|
2024-04-16 21:58:39 +00:00
|
|
|
"net/http"
|
2024-07-31 18:17:58 +00:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2025-01-21 14:53:18 +00:00
|
|
|
"strconv"
|
2025-01-20 18:54:03 +00:00
|
|
|
"strings"
|
2024-08-02 21:48:26 +00:00
|
|
|
"time"
|
2024-04-16 21:58:39 +00:00
|
|
|
|
2024-09-03 07:07:45 +00:00
|
|
|
"arimelody-web/admin"
|
|
|
|
"arimelody-web/api"
|
2025-01-21 14:53:18 +00:00
|
|
|
"arimelody-web/colour"
|
2024-11-01 21:03:08 +00:00
|
|
|
"arimelody-web/controller"
|
2025-01-20 23:49:54 +00:00
|
|
|
"arimelody-web/model"
|
2024-09-03 07:07:45 +00:00
|
|
|
"arimelody-web/templates"
|
2025-01-20 18:54:03 +00:00
|
|
|
"arimelody-web/view"
|
2025-02-07 12:41:25 +00:00
|
|
|
"arimelody-web/log"
|
2024-08-02 21:48:26 +00:00
|
|
|
|
|
|
|
"github.com/jmoiron/sqlx"
|
2024-08-02 23:27:30 +00:00
|
|
|
_ "github.com/lib/pq"
|
2025-01-23 09:39:40 +00:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
2024-03-18 10:34:43 +00:00
|
|
|
)
|
|
|
|
|
2025-01-20 18:54:03 +00:00
|
|
|
// used for database migrations
|
|
|
|
const DB_VERSION = 1
|
|
|
|
|
2024-11-09 23:36:18 +00:00
|
|
|
const DEFAULT_PORT int64 = 8080
|
2024-03-18 10:34:43 +00:00
|
|
|
|
|
|
|
func main() {
|
2025-01-20 18:54:03 +00:00
|
|
|
fmt.Printf("made with <3 by ari melody\n\n")
|
|
|
|
|
2025-01-21 14:53:18 +00:00
|
|
|
app := model.AppState{
|
|
|
|
Config: controller.GetConfig(),
|
|
|
|
}
|
2025-01-21 14:12:21 +00:00
|
|
|
|
2024-08-02 21:48:26 +00:00
|
|
|
// initialise database connection
|
2025-01-21 14:53:18 +00:00
|
|
|
if app.Config.DB.Host == "" {
|
2024-11-10 05:34:04 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: db.host not provided! Exiting...\n")
|
2024-11-09 23:36:18 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
2025-01-21 14:53:18 +00:00
|
|
|
if app.Config.DB.Name == "" {
|
2024-11-10 05:34:04 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: db.name not provided! Exiting...\n")
|
2024-11-09 23:36:18 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
2025-01-21 14:53:18 +00:00
|
|
|
if app.Config.DB.User == "" {
|
2024-11-10 05:34:04 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: db.user not provided! Exiting...\n")
|
2024-11-09 23:36:18 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
2025-01-21 14:53:18 +00:00
|
|
|
if app.Config.DB.Pass == "" {
|
2024-11-10 05:34:04 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: db.pass not provided! Exiting...\n")
|
2024-11-09 23:36:18 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
2024-09-03 07:07:45 +00:00
|
|
|
|
2024-07-31 03:09:22 +00:00
|
|
|
var err error
|
2025-01-21 14:53:18 +00:00
|
|
|
app.DB, err = sqlx.Connect(
|
2024-11-10 05:34:04 +00:00
|
|
|
"postgres",
|
|
|
|
fmt.Sprintf(
|
2025-01-21 14:12:21 +00:00
|
|
|
"host=%s port=%d user=%s dbname=%s password='%s' sslmode=disable",
|
2025-01-21 14:53:18 +00:00
|
|
|
app.Config.DB.Host,
|
|
|
|
app.Config.DB.Port,
|
|
|
|
app.Config.DB.User,
|
|
|
|
app.Config.DB.Name,
|
|
|
|
app.Config.DB.Pass,
|
2024-11-10 05:34:04 +00:00
|
|
|
),
|
|
|
|
)
|
2024-08-02 21:48:26 +00:00
|
|
|
if err != nil {
|
2024-09-22 23:57:23 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Unable to initialise database: %v\n", err)
|
2024-08-02 21:48:26 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
2025-01-21 14:53:18 +00:00
|
|
|
app.DB.SetConnMaxLifetime(time.Minute * 3)
|
|
|
|
app.DB.SetMaxOpenConns(10)
|
|
|
|
app.DB.SetMaxIdleConns(10)
|
|
|
|
defer app.DB.Close()
|
2024-08-03 22:24:15 +00:00
|
|
|
|
2025-01-20 18:54:03 +00:00
|
|
|
// handle command arguments
|
|
|
|
if len(os.Args) > 1 {
|
|
|
|
arg := os.Args[1]
|
|
|
|
|
|
|
|
switch arg {
|
2025-01-20 23:49:54 +00:00
|
|
|
case "createTOTP":
|
|
|
|
if len(os.Args) < 4 {
|
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: `username` and `name` must be specified for createTOTP.\n")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
username := os.Args[2]
|
|
|
|
totpName := os.Args[3]
|
|
|
|
|
2025-01-23 00:37:19 +00:00
|
|
|
account, err := controller.GetAccountByUsername(app.DB, username)
|
2025-01-20 23:49:54 +00:00
|
|
|
if err != nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
2025-01-20 23:49:54 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
if account == nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
|
2025-01-20 23:49:54 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
2025-01-26 00:48:19 +00:00
|
|
|
secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH)
|
2025-01-20 23:49:54 +00:00
|
|
|
totp := model.TOTP {
|
|
|
|
AccountID: account.ID,
|
|
|
|
Name: totpName,
|
|
|
|
Secret: string(secret),
|
|
|
|
}
|
|
|
|
|
2025-01-21 14:53:18 +00:00
|
|
|
err = controller.CreateTOTP(app.DB, &totp)
|
2025-01-20 23:49:54 +00:00
|
|
|
if err != nil {
|
2025-01-23 00:37:19 +00:00
|
|
|
if strings.HasPrefix(err.Error(), "pq: duplicate key") {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" already has a TOTP method named \"%s\"!\n", account.Username, totp.Name)
|
2025-01-23 00:37:19 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to create TOTP method: %v\n", err)
|
2025-01-20 23:49:54 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
url := controller.GenerateTOTPURI(account.Username, totp.Secret)
|
|
|
|
fmt.Printf("%s\n", url)
|
|
|
|
return
|
|
|
|
|
|
|
|
case "deleteTOTP":
|
|
|
|
if len(os.Args) < 4 {
|
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: `username` and `name` must be specified for deleteTOTP.\n")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
username := os.Args[2]
|
|
|
|
totpName := os.Args[3]
|
|
|
|
|
2025-01-23 00:37:19 +00:00
|
|
|
account, err := controller.GetAccountByUsername(app.DB, username)
|
2025-01-20 23:49:54 +00:00
|
|
|
if err != nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
2025-01-20 23:49:54 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
if account == nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
|
2025-01-20 23:49:54 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
2025-01-21 14:53:18 +00:00
|
|
|
err = controller.DeleteTOTP(app.DB, account.ID, totpName)
|
2025-01-20 23:49:54 +00:00
|
|
|
if err != nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to create TOTP method: %v\n", err)
|
2025-01-20 23:49:54 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("TOTP method \"%s\" deleted.\n", totpName)
|
|
|
|
return
|
|
|
|
|
|
|
|
case "listTOTP":
|
|
|
|
if len(os.Args) < 3 {
|
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for listTOTP.\n")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
username := os.Args[2]
|
|
|
|
|
2025-01-23 00:37:19 +00:00
|
|
|
account, err := controller.GetAccountByUsername(app.DB, username)
|
2025-01-20 23:49:54 +00:00
|
|
|
if err != nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
2025-01-20 23:49:54 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
if account == nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
|
2025-01-20 23:49:54 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
2025-01-21 14:53:18 +00:00
|
|
|
totps, err := controller.GetTOTPsForAccount(app.DB, account.ID)
|
2025-01-20 23:49:54 +00:00
|
|
|
if err != nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to create TOTP methods: %v\n", err)
|
2025-01-20 23:49:54 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
for i, totp := range totps {
|
|
|
|
fmt.Printf("%d. %s - Created %s\n", i + 1, totp.Name, totp.CreatedAt)
|
|
|
|
}
|
|
|
|
if len(totps) == 0 {
|
|
|
|
fmt.Printf("\"%s\" has no TOTP methods.\n", account.Username)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
|
|
|
|
case "testTOTP":
|
|
|
|
if len(os.Args) < 4 {
|
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: `username` and `name` must be specified for testTOTP.\n")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
username := os.Args[2]
|
|
|
|
totpName := os.Args[3]
|
|
|
|
|
2025-01-23 00:37:19 +00:00
|
|
|
account, err := controller.GetAccountByUsername(app.DB, username)
|
2025-01-20 23:49:54 +00:00
|
|
|
if err != nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
2025-01-20 23:49:54 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
if account == nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
|
2025-01-20 23:49:54 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
2025-01-21 14:53:18 +00:00
|
|
|
totp, err := controller.GetTOTP(app.DB, account.ID, totpName)
|
2025-01-20 23:49:54 +00:00
|
|
|
if err != nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch TOTP method \"%s\": %v\n", totpName, err)
|
2025-01-20 23:49:54 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
if totp == nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: TOTP method \"%s\" does not exist for account \"%s\"\n", totpName, username)
|
2025-01-20 23:49:54 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
code := controller.GenerateTOTP(totp.Secret, 0)
|
|
|
|
fmt.Printf("%s\n", code)
|
|
|
|
return
|
2025-01-26 20:37:20 +00:00
|
|
|
|
|
|
|
case "cleanTOTP":
|
|
|
|
err := controller.DeleteUnconfirmedTOTPs(app.DB)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to clean up TOTP methods: %v\n", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
fmt.Printf("Cleaned up dangling TOTP methods successfully.\n")
|
|
|
|
return
|
2025-01-20 23:49:54 +00:00
|
|
|
|
2025-01-20 18:54:03 +00:00
|
|
|
case "createInvite":
|
|
|
|
fmt.Printf("Creating invite...\n")
|
2025-01-21 14:53:18 +00:00
|
|
|
invite, err := controller.CreateInvite(app.DB, 16, time.Hour * 24)
|
2025-01-20 18:54:03 +00:00
|
|
|
if err != nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite code: %v\n", err)
|
2025-01-20 18:54:03 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Printf(
|
|
|
|
"Here you go! This code expires in %d hours: %s\n",
|
|
|
|
int(math.Ceil(invite.ExpiresAt.Sub(invite.CreatedAt).Hours())),
|
|
|
|
invite.Code,
|
|
|
|
)
|
2025-01-20 18:54:03 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
case "purgeInvites":
|
|
|
|
fmt.Printf("Deleting all invites...\n")
|
2025-01-21 14:53:18 +00:00
|
|
|
err := controller.DeleteAllInvites(app.DB)
|
2025-01-20 18:54:03 +00:00
|
|
|
if err != nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to delete invites: %v\n", err)
|
2025-01-20 18:54:03 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("Invites deleted successfully.\n")
|
|
|
|
return
|
|
|
|
|
2025-01-20 19:11:16 +00:00
|
|
|
case "listAccounts":
|
2025-01-21 14:53:18 +00:00
|
|
|
accounts, err := controller.GetAllAccounts(app.DB)
|
2025-01-20 19:11:16 +00:00
|
|
|
if err != nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch accounts: %v\n", err)
|
2025-01-20 19:11:16 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, account := range accounts {
|
2025-01-23 00:37:19 +00:00
|
|
|
email := "<none>"
|
|
|
|
if account.Email.Valid { email = account.Email.String }
|
2025-01-20 19:11:16 +00:00
|
|
|
fmt.Printf(
|
|
|
|
"User: %s\n" +
|
|
|
|
"\tID: %s\n" +
|
|
|
|
"\tEmail: %s\n" +
|
|
|
|
"\tCreated: %s\n",
|
|
|
|
account.Username,
|
|
|
|
account.ID,
|
2025-01-23 00:37:19 +00:00
|
|
|
email,
|
2025-01-20 19:11:16 +00:00
|
|
|
account.CreatedAt,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
|
2025-01-23 09:39:40 +00:00
|
|
|
case "changePassword":
|
|
|
|
if len(os.Args) < 4 {
|
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: `username` and `password` must be specified for changePassword\n")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
username := os.Args[2]
|
|
|
|
password := os.Args[3]
|
|
|
|
account, err := controller.GetAccountByUsername(app.DB, username)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
if account == nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to update password: %v\n", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
account.Password = string(hashedPassword)
|
|
|
|
err = controller.UpdateAccount(app.DB, account)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to delete account: %v\n", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("Account \"%s\" deleted successfully.\n", account.Username)
|
|
|
|
return
|
|
|
|
|
2025-01-20 18:54:03 +00:00
|
|
|
case "deleteAccount":
|
2025-01-20 23:49:54 +00:00
|
|
|
if len(os.Args) < 3 {
|
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for deleteAccount\n")
|
2025-01-20 18:54:03 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
username := os.Args[2]
|
|
|
|
fmt.Printf("Deleting account \"%s\"...\n", username)
|
|
|
|
|
2025-01-23 00:37:19 +00:00
|
|
|
account, err := controller.GetAccountByUsername(app.DB, username)
|
2025-01-20 18:54:03 +00:00
|
|
|
if err != nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
2025-01-20 18:54:03 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
if account == nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
|
2025-01-20 18:54:03 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2025-01-23 09:39:40 +00:00
|
|
|
err = controller.DeleteAccount(app.DB, account.ID)
|
2025-01-20 18:54:03 +00:00
|
|
|
if err != nil {
|
2025-01-23 09:39:40 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to delete account: %v\n", err)
|
2025-01-20 18:54:03 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("Account \"%s\" deleted successfully.\n", account.Username)
|
|
|
|
return
|
2025-02-07 12:41:25 +00:00
|
|
|
|
|
|
|
case "testLogSearch":
|
|
|
|
// TODO: rename to "logs"; add parameters
|
|
|
|
logger := log.Logger { DB: app.DB }
|
|
|
|
logs, err := logger.Search([]log.LogLevel{ log.LEVEL_INFO, log.LEVEL_WARN }, []string{ log.TYPE_ACCOUNT, log.TYPE_MUSIC }, "ari", 0, 100)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch logs: %v\n", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
for _, log := range(logs) {
|
|
|
|
fmt.Printf("[%s] [%s] [%d] [%s] %s\n", log.CreatedAt.Format(time.UnixDate), log.ID, log.Level, log.Type, log.Content)
|
|
|
|
}
|
|
|
|
return
|
2025-01-20 18:54:03 +00:00
|
|
|
}
|
|
|
|
|
2025-01-20 19:11:16 +00:00
|
|
|
// command help
|
|
|
|
fmt.Print(
|
2025-01-20 18:54:03 +00:00
|
|
|
"Available commands:\n\n" +
|
2025-01-20 23:49:54 +00:00
|
|
|
"createTOTP <username> <name>:\n\tCreates a timed one-time passcode method.\n" +
|
|
|
|
"listTOTP <username>:\n\tLists an account's TOTP methods.\n" +
|
|
|
|
"deleteTOTP <username> <name>:\n\tDeletes an account's TOTP method.\n" +
|
|
|
|
"testTOTP <username> <name>:\n\tGenerates the code for an account's TOTP method.\n" +
|
2025-01-26 20:37:20 +00:00
|
|
|
"cleanTOTP:\n\tCleans up unconfirmed (dangling) TOTP methods.\n" +
|
2025-01-20 23:49:54 +00:00
|
|
|
"\n" +
|
2025-01-20 18:54:03 +00:00
|
|
|
"createInvite:\n\tCreates an invite code to register new accounts.\n" +
|
|
|
|
"purgeInvites:\n\tDeletes all available invite codes.\n" +
|
2025-01-20 19:11:16 +00:00
|
|
|
"listAccounts:\n\tLists all active accounts.\n",
|
2025-01-20 18:54:03 +00:00
|
|
|
"deleteAccount <username>:\n\tDeletes an account with a given `username`.\n",
|
|
|
|
)
|
|
|
|
return
|
2024-09-22 23:57:23 +00:00
|
|
|
}
|
|
|
|
|
2025-01-20 18:54:03 +00:00
|
|
|
// handle DB migrations
|
2025-01-21 14:53:18 +00:00
|
|
|
controller.CheckDBVersionAndMigrate(app.DB)
|
2025-01-20 18:54:03 +00:00
|
|
|
|
|
|
|
// initial invite code
|
2024-09-22 23:57:23 +00:00
|
|
|
accountsCount := 0
|
2025-01-21 14:53:18 +00:00
|
|
|
err = app.DB.Get(&accountsCount, "SELECT count(*) FROM account")
|
2025-01-20 18:54:03 +00:00
|
|
|
if err != nil { panic(err) }
|
2024-09-22 23:57:23 +00:00
|
|
|
if accountsCount == 0 {
|
2025-01-21 14:53:18 +00:00
|
|
|
_, err := app.DB.Exec("DELETE FROM invite")
|
2024-09-22 23:57:23 +00:00
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to clear existing invite codes: %v\n", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
2025-01-20 18:54:03 +00:00
|
|
|
|
2025-01-21 14:53:18 +00:00
|
|
|
invite, err := controller.CreateInvite(app.DB, 16, time.Hour * 24)
|
2024-09-22 23:57:23 +00:00
|
|
|
if err != nil {
|
2025-01-20 18:54:03 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite code: %v\n", err)
|
2024-09-22 23:57:23 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
2025-01-21 14:12:21 +00:00
|
|
|
fmt.Printf("No accounts exist! Generated invite code: %s\n", invite.Code)
|
2025-01-20 18:54:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// delete expired invites
|
2025-01-21 14:53:18 +00:00
|
|
|
err = controller.DeleteExpiredInvites(app.DB)
|
2025-01-20 18:54:03 +00:00
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to clear expired invite codes: %v\n", err)
|
|
|
|
os.Exit(1)
|
2024-09-22 23:57:23 +00:00
|
|
|
}
|
|
|
|
|
2025-01-26 20:37:20 +00:00
|
|
|
// clean up unconfirmed TOTP methods
|
|
|
|
err = controller.DeleteUnconfirmedTOTPs(app.DB)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to clean up unconfirmed TOTP methods: %v\n", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
2024-08-02 21:48:26 +00:00
|
|
|
// start the web server!
|
2025-01-21 14:53:18 +00:00
|
|
|
mux := createServeMux(&app)
|
2025-01-21 17:13:06 +00:00
|
|
|
fmt.Printf("Now serving at http://%s:%d\n", app.Config.Host, app.Config.Port)
|
2025-02-07 12:41:25 +00:00
|
|
|
stdLog.Fatal(
|
2025-01-21 17:13:06 +00:00
|
|
|
http.ListenAndServe(fmt.Sprintf("%s:%d", app.Config.Host, app.Config.Port),
|
2025-01-21 14:53:18 +00:00
|
|
|
HTTPLog(DefaultHeaders(mux)),
|
2024-12-07 19:25:45 +00:00
|
|
|
))
|
2024-08-01 00:39:18 +00:00
|
|
|
}
|
2024-07-31 03:09:22 +00:00
|
|
|
|
2025-01-21 14:53:18 +00:00
|
|
|
func createServeMux(app *model.AppState) *http.ServeMux {
|
2024-07-31 03:09:22 +00:00
|
|
|
mux := http.NewServeMux()
|
2024-09-22 23:57:23 +00:00
|
|
|
|
2025-01-21 14:53:18 +00:00
|
|
|
mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler(app)))
|
|
|
|
mux.Handle("/api/", http.StripPrefix("/api", api.Handler(app)))
|
|
|
|
mux.Handle("/music/", http.StripPrefix("/music", view.MusicHandler(app)))
|
|
|
|
mux.Handle("/uploads/", http.StripPrefix("/uploads", staticHandler(filepath.Join(app.Config.DataDirectory, "uploads"))))
|
2024-08-02 21:48:26 +00:00
|
|
|
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2024-11-10 05:44:45 +00:00
|
|
|
if r.Method == http.MethodHead {
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-07-31 18:17:58 +00:00
|
|
|
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
|
2025-01-26 20:09:18 +00:00
|
|
|
err := templates.IndexTemplate.Execute(w, nil)
|
2024-09-01 03:43:32 +00:00
|
|
|
if err != nil {
|
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
}
|
2024-07-31 18:17:58 +00:00
|
|
|
return
|
|
|
|
}
|
2024-08-01 02:54:15 +00:00
|
|
|
staticHandler("public").ServeHTTP(w, r)
|
2024-08-02 21:48:26 +00:00
|
|
|
}))
|
2024-09-22 23:57:23 +00:00
|
|
|
|
2024-08-01 00:39:18 +00:00
|
|
|
return mux
|
2024-03-18 10:34:43 +00:00
|
|
|
}
|
2024-07-31 12:45:34 +00:00
|
|
|
|
2024-08-01 02:54:15 +00:00
|
|
|
func staticHandler(directory string) http.Handler {
|
2024-07-31 18:17:58 +00:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2024-08-01 02:54:15 +00:00
|
|
|
info, err := os.Stat(filepath.Join(directory, filepath.Clean(r.URL.Path)))
|
2024-09-03 07:07:45 +00:00
|
|
|
|
2024-07-31 18:17:58 +00:00
|
|
|
// does the file exist?
|
|
|
|
if err != nil {
|
2024-09-03 07:07:45 +00:00
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
2024-07-31 18:17:58 +00:00
|
|
|
http.NotFound(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// is thjs a directory? (forbidden)
|
|
|
|
if info.IsDir() {
|
|
|
|
http.NotFound(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-08-02 21:48:26 +00:00
|
|
|
http.FileServer(http.Dir(directory)).ServeHTTP(w, r)
|
2024-07-31 18:17:58 +00:00
|
|
|
})
|
|
|
|
}
|
2025-01-21 14:53:18 +00:00
|
|
|
|
|
|
|
var PoweredByStrings = []string{
|
|
|
|
"nerd rage",
|
|
|
|
"estrogen",
|
|
|
|
"your mother",
|
|
|
|
"awesome powers beyond comprehension",
|
|
|
|
"jared",
|
|
|
|
"the weight of my sins",
|
|
|
|
"the arc reactor",
|
|
|
|
"AA batteries",
|
|
|
|
"15 euro solar panel from ebay",
|
|
|
|
"magnets, how do they work",
|
|
|
|
"a fax machine",
|
|
|
|
"dell optiplex",
|
|
|
|
"a trans girl's nintendo wii",
|
|
|
|
"BASS",
|
|
|
|
"electricity, duh",
|
|
|
|
"seven hamsters in a big wheel",
|
|
|
|
"girls",
|
|
|
|
"mzungu hosting",
|
|
|
|
"golang",
|
|
|
|
"the state of the world right now",
|
|
|
|
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)",
|
|
|
|
"the good folks at aperture science",
|
|
|
|
"free2play CDs",
|
|
|
|
"aridoodle",
|
|
|
|
"the love of creating",
|
|
|
|
"not for the sake of art; not for the sake of money; we like painting naked people",
|
|
|
|
"30 billion dollars in VC funding",
|
|
|
|
}
|
|
|
|
|
|
|
|
func DefaultHeaders(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.Header().Add("Server", "arimelody.me")
|
|
|
|
w.Header().Add("Do-Not-Stab", "1")
|
|
|
|
w.Header().Add("X-Clacks-Overhead", "GNU Terry Pratchett")
|
|
|
|
w.Header().Add("X-Hacker", "spare me please")
|
|
|
|
w.Header().Add("X-Robots-TXT", "'; DROP TABLE pages;")
|
|
|
|
w.Header().Add("X-Thinking-With", "Portals")
|
|
|
|
w.Header().Add(
|
|
|
|
"X-Powered-By",
|
|
|
|
PoweredByStrings[rand.Intn(len(PoweredByStrings))],
|
|
|
|
)
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
type LoggingResponseWriter struct {
|
|
|
|
http.ResponseWriter
|
|
|
|
Status int
|
|
|
|
}
|
|
|
|
|
|
|
|
func (lrw *LoggingResponseWriter) WriteHeader(status int) {
|
|
|
|
lrw.Status = status
|
|
|
|
lrw.ResponseWriter.WriteHeader(status)
|
|
|
|
}
|
|
|
|
|
|
|
|
func HTTPLog(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
start := time.Now()
|
|
|
|
|
|
|
|
lrw := LoggingResponseWriter{w, http.StatusOK}
|
|
|
|
|
|
|
|
next.ServeHTTP(&lrw, r)
|
|
|
|
|
|
|
|
after := time.Now()
|
|
|
|
difference := (after.Nanosecond() - start.Nanosecond()) / 1_000_000
|
|
|
|
elapsed := "<1"
|
|
|
|
if difference >= 1 {
|
|
|
|
elapsed = strconv.Itoa(difference)
|
|
|
|
}
|
|
|
|
|
|
|
|
statusColour := colour.Reset
|
|
|
|
|
|
|
|
if lrw.Status - 600 <= 0 { statusColour = colour.Red }
|
|
|
|
if lrw.Status - 500 <= 0 { statusColour = colour.Yellow }
|
|
|
|
if lrw.Status - 400 <= 0 { statusColour = colour.White }
|
|
|
|
if lrw.Status - 300 <= 0 { statusColour = colour.Green }
|
|
|
|
|
|
|
|
fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n",
|
|
|
|
after.Format(time.UnixDate),
|
|
|
|
r.Method,
|
|
|
|
r.URL.Path,
|
|
|
|
statusColour,
|
|
|
|
lrw.Status,
|
|
|
|
colour.Reset,
|
|
|
|
elapsed,
|
|
|
|
r.Header["User-Agent"][0])
|
|
|
|
})
|
|
|
|
}
|