package main import ( "errors" "fmt" "log" "net/http" "os" "path/filepath" "time" "arimelody-web/account/model" "arimelody-web/admin" "arimelody-web/api" "arimelody-web/global" "arimelody-web/view" "arimelody-web/templates" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" ) const DEFAULT_PORT int = 8080 func main() { // initialise database connection var dbHost = os.Getenv("ARIMELODY_DB_HOST") if dbHost == "" { dbHost = "127.0.0.1" } var err error global.DB, err = initDB("postgres", "host=" + dbHost + " user=arimelody dbname=arimelody password=fuckingpassword sslmode=disable") 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() _, err = global.DB.Exec("DELETE FROM invite WHERE expires_at < CURRENT_TIMESTAMP") if err != nil { fmt.Fprintf(os.Stderr, "FATAL: Failed to clear expired invite codes: %v\n", err) os.Exit(1) } accountsCount := 0 global.DB.Get(&accountsCount, "SELECT count(*) FROM account") if accountsCount == 0 { code := model.GenerateInviteCode(8) tx, err := global.DB.Begin() if err != nil { fmt.Fprintf(os.Stderr, "FATAL: Failed to begin transaction: %v\n", err) os.Exit(1) } _, err = tx.Exec("DELETE FROM invite") if err != nil { fmt.Fprintf(os.Stderr, "FATAL: Failed to clear existing invite codes: %v\n", err) os.Exit(1) } _, err = tx.Exec("INSERT INTO invite (code,expires_at) VALUES ($1, $2)", code, time.Now().Add(60 * time.Minute)) if err != nil { fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite codes: %v\n", err) os.Exit(1) } err = tx.Commit() if err != nil { fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite codes: %v\n", err) os.Exit(1) } fmt.Fprintln(os.Stdout, "INFO: No accounts exist! Generated invite code: " + string(code) + " (Use this at /register or /api/v1/register)") } // start the web server! mux := createServeMux() port := DEFAULT_PORT fmt.Printf("Now serving at http://127.0.0.1:%d\n", port) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), global.HTTPLog(mux))) } func initDB(driverName string, dataSourceName string) (*sqlx.DB, error) { db, err := sqlx.Connect(driverName, dataSourceName) if err != nil { return nil, err } // ensure tables exist // account _, err = db.Exec( "CREATE TABLE IF NOT EXISTS account (" + "id uuid PRIMARY KEY DEFAULT gen_random_uuid(), " + "username text NOT NULL UNIQUE, " + "password text NOT NULL, " + "email text, " + "avatar_url text)", ) if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create account table: %s", err.Error())) } // privilege _, err = db.Exec( "CREATE TABLE IF NOT EXISTS privilege (" + "account uuid NOT NULL, " + "privilege text NOT NULL, " + "CONSTRAINT privilege_pk PRIMARY KEY (account, privilege), " + "CONSTRAINT privilege_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE)", ) if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create privilege table: %s", err.Error())) } // totp _, err = db.Exec( "CREATE TABLE IF NOT EXISTS totp (" + "account uuid NOT NULL, " + "name text NOT NULL, " + "created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "CONSTRAINT totp_pk PRIMARY KEY (account, name), " + "CONSTRAINT totp_account_fk FOREIGN KEY (account) REFERENCES account(id) ON DELETE CASCADE)", ) if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create TOTP table: %s", err.Error())) } // invites _, err = db.Exec( "CREATE TABLE IF NOT EXISTS invite (" + "code text NOT NULL PRIMARY KEY, " + "created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " + "expires_at TIMESTAMP NOT NULL)", ) if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create TOTP table: %s", err.Error())) } // artist _, err = db.Exec( "CREATE TABLE IF NOT EXISTS artist (" + "id character varying(64) PRIMARY KEY, " + "name text NOT NULL, " + "website text, " + "avatar text)", ) if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create artist table: %s", err.Error())) } // musicrelease _, err = db.Exec( "CREATE TABLE IF NOT EXISTS musicrelease (" + "id character varying(64) PRIMARY KEY, " + "visible bool DEFAULT false, " + "title text NOT NULL, " + "description text, " + "type text, " + "release_date TIMESTAMP NOT NULL, " + "artwork text, " + "buyname text, " + "buylink text, " + "copyright text, " + "copyrightURL text)", ) if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musicrelease table: %s", err.Error())) } // musiclink _, err = db.Exec( "CREATE TABLE IF NOT EXISTS public.musiclink (" + "release character varying(64) NOT NULL, " + "name text NOT NULL, " + "url text NOT NULL, " + "CONSTRAINT musiclink_pk PRIMARY KEY (release, name), " + "CONSTRAINT musiclink_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE)", ) if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musiclink table: %s", err.Error())) } // musiccredit _, err = db.Exec( "CREATE TABLE IF NOT EXISTS public.musiccredit (" + "release character varying(64) NOT NULL, " + "artist character varying(64) NOT NULL, " + "role text NOT NULL, " + "is_primary boolean DEFAULT false, " + "CONSTRAINT musiccredit_pk PRIMARY KEY (release, artist), " + "CONSTRAINT musiccredit_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE, " + "CONSTRAINT musiccredit_artist_fk FOREIGN KEY (artist) REFERENCES artist(id) ON DELETE CASCADE)", ) if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musiccredit table: %s", err.Error())) } // musictrack _, err = db.Exec( "CREATE TABLE IF NOT EXISTS public.musictrack (" + "id uuid DEFAULT gen_random_uuid() PRIMARY KEY, " + "title text NOT NULL, " + "description text, " + "lyrics text, " + "preview_url text)", ) if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musictrack table: %s", err.Error())) } // musicreleasetrack _, err = db.Exec( "CREATE TABLE IF NOT EXISTS public.musicreleasetrack (" + "release character varying(64) NOT NULL, " + "track uuid NOT NULL, " + "number integer NOT NULL, " + "CONSTRAINT musicreleasetrack_pk PRIMARY KEY (release, track), " + "CONSTRAINT musicreleasetrack_release_fk FOREIGN KEY (release) REFERENCES musicrelease(id) ON DELETE CASCADE, " + "CONSTRAINT musicreleasetrack_artist_fk FOREIGN KEY (track) REFERENCES track(id) ON DELETE CASCADE)", ) if err != nil { return nil, errors.New(fmt.Sprintf("Failed to create musicreleasetrack table: %s", err.Error())) } // TODO: automatic database migration return db, nil } 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("uploads"))) mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 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) }) }