"embed"
"flag"
"fmt"
+ "golang.org/x/crypto/bcrypt"
"golang.org/x/net/websocket"
"html/template"
"io"
//go:embed templates/*
var content embed.FS
+
//var content = os.DirFS("../broadcaster-server/")
var config ServerConfig = NewServerConfig()
// Authenticated routes
- http.HandleFunc("/", homePage)
- http.HandleFunc("/logout", logOutPage)
- http.HandleFunc("/change-password", changePasswordPage)
+ http.Handle("/", requireUser(homePage))
+ http.Handle("/logout", requireUser(logOutPage))
+ http.Handle("/change-password", requireUser(changePasswordPage))
- http.HandleFunc("/playlists/", playlistSection)
- http.HandleFunc("/files/", fileSection)
- http.HandleFunc("/radios/", radioSection)
+ http.Handle("/playlists/", requireUser(playlistSection))
+ http.Handle("/files/", requireUser(fileSection))
+ http.Handle("/radios/", requireUser(radioSection))
- http.Handle("/radio-ws", websocket.Handler(RadioSync))
- http.Handle("/web-ws", websocket.Handler(WebSync))
- http.HandleFunc("/stop", stopPage)
+ http.Handle("/stop", requireUser(stopPage))
// Admin routes
+ http.Handle("/users/", requireAdmin(userSection))
+
+ // Websocket routes, which perform their own auth
+
+ http.Handle("/radio-ws", websocket.Handler(RadioSync))
+ http.Handle("/web-ws", websocket.Handler(WebSync))
+
err := http.ListenAndServe(config.BindAddress+":"+strconv.Itoa(config.Port), nil)
if err != nil {
log.Fatal(err)
}
}
+type authenticatedHandler func(http.ResponseWriter, *http.Request, User)
+
+type AuthMiddleware struct {
+ handler authenticatedHandler
+ mustBeAdmin bool
+}
+
+func (m AuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ user, err := currentUser(w, r)
+ if err != nil || (m.mustBeAdmin && !user.IsAdmin) {
+ http.Redirect(w, r, "/login", http.StatusFound)
+ return
+ }
+ m.handler(w, r, user)
+}
+
+func requireUser(handler authenticatedHandler) AuthMiddleware {
+ return AuthMiddleware{
+ handler: handler,
+ mustBeAdmin: false,
+ }
+}
+
+func requireAdmin(handler authenticatedHandler) AuthMiddleware {
+ return AuthMiddleware{
+ handler: handler,
+ mustBeAdmin: true,
+ }
+}
+
type HeaderData struct {
SelectedMenu string
- Username string
+ User User
}
-func renderHeader(w http.ResponseWriter, selectedMenu string) {
+func renderHeader(w http.ResponseWriter, selectedMenu string, user User) {
tmpl := template.Must(template.ParseFS(content, "templates/header.html"))
data := HeaderData{
SelectedMenu: selectedMenu,
- Username: "username",
+ User: user,
}
err := tmpl.Execute(w, data)
if err != nil {
Username string
}
-func homePage(w http.ResponseWriter, r *http.Request) {
- renderHeader(w, "status")
+func homePage(w http.ResponseWriter, r *http.Request, user User) {
+ renderHeader(w, "status", user)
tmpl := template.Must(template.ParseFS(content, "templates/index.html"))
data := HomeData{
LoggedIn: true,
}
func logInPage(w http.ResponseWriter, r *http.Request) {
- log.Println("Log in page!")
r.ParseForm()
username := r.Form["username"]
password := r.Form["password"]
data := LogInData{
Error: errText,
}
- renderHeader(w, "")
+ renderHeader(w, "", User{})
tmpl := template.Must(template.ParseFS(content, "templates/login.html"))
tmpl.Execute(w, data)
renderFooter(w)
}
-func playlistSection(w http.ResponseWriter, r *http.Request) {
+func playlistSection(w http.ResponseWriter, r *http.Request, user User) {
path := strings.Split(r.URL.Path, "/")
if len(path) != 3 {
http.NotFound(w, r)
return
}
if path[2] == "new" {
- editPlaylistPage(w, r, 0)
+ editPlaylistPage(w, r, 0, user)
} else if path[2] == "submit" && r.Method == "POST" {
submitPlaylist(w, r)
} else if path[2] == "delete" && r.Method == "POST" {
deletePlaylist(w, r)
} else if path[2] == "" {
- playlistsPage(w, r)
+ playlistsPage(w, r, user)
} else {
id, err := strconv.Atoi(path[2])
if err != nil {
http.NotFound(w, r)
return
}
- editPlaylistPage(w, r, id)
+ editPlaylistPage(w, r, id, user)
}
}
-func fileSection(w http.ResponseWriter, r *http.Request) {
+func fileSection(w http.ResponseWriter, r *http.Request, user User) {
path := strings.Split(r.URL.Path, "/")
if len(path) != 3 {
http.NotFound(w, r)
} else if path[2] == "delete" && r.Method == "POST" {
deleteFile(w, r)
} else if path[2] == "" {
- filesPage(w, r)
+ filesPage(w, r, user)
} else {
http.NotFound(w, r)
return
}
}
-func radioSection(w http.ResponseWriter, r *http.Request) {
+func radioSection(w http.ResponseWriter, r *http.Request, user User) {
path := strings.Split(r.URL.Path, "/")
if len(path) != 3 {
http.NotFound(w, r)
return
}
if path[2] == "new" {
- editRadioPage(w, r, 0)
+ editRadioPage(w, r, 0, user)
} else if path[2] == "submit" && r.Method == "POST" {
submitRadio(w, r)
} else if path[2] == "delete" && r.Method == "POST" {
deleteRadio(w, r)
} else if path[2] == "" {
- radiosPage(w, r)
+ radiosPage(w, r, user)
+ } else {
+ id, err := strconv.Atoi(path[2])
+ if err != nil {
+ http.NotFound(w, r)
+ return
+ }
+ editRadioPage(w, r, id, user)
+ }
+}
+
+func userSection(w http.ResponseWriter, r *http.Request, user User) {
+ path := strings.Split(r.URL.Path, "/")
+ if len(path) != 3 {
+ http.NotFound(w, r)
+ return
+ }
+ if path[2] == "new" {
+ editUserPage(w, r, 0, user)
+ } else if path[2] == "submit" && r.Method == "POST" {
+ submitUser(w, r)
+ } else if path[2] == "delete" && r.Method == "POST" {
+ deleteUser(w, r)
+ } else if path[2] == "reset-password" && r.Method == "POST" {
+ resetUserPassword(w, r)
+ } else if path[2] == "" {
+ usersPage(w, r, user)
} else {
id, err := strconv.Atoi(path[2])
if err != nil {
http.NotFound(w, r)
return
}
- editRadioPage(w, r, id)
+ editUserPage(w, r, id, user)
+ }
+}
+
+type EditUserPageData struct {
+ User User
+}
+
+func editUserPage(w http.ResponseWriter, r *http.Request, id int, user User) {
+ var data EditUserPageData
+ if id != 0 {
+ user, err := db.GetUserById(id)
+ if err != nil {
+ http.NotFound(w, r)
+ return
+ }
+ data.User = user
+ }
+ renderHeader(w, "users", user)
+ tmpl := template.Must(template.ParseFS(content, "templates/user.html"))
+ tmpl.Execute(w, data)
+ renderFooter(w)
+}
+
+func submitUser(w http.ResponseWriter, r *http.Request) {
+ err := r.ParseForm()
+ if err == nil {
+ id, err := strconv.Atoi(r.Form.Get("userId"))
+ if err != nil {
+ return
+ }
+ if id == 0 {
+ newPassword := r.Form.Get("password")
+ hashed, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
+ if err != nil {
+ return
+ }
+ user := User{
+ Id: 0,
+ Username: r.Form.Get("username"),
+ IsAdmin: r.Form.Get("isAdmin") == "1",
+ PasswordHash: string(hashed),
+ }
+ db.CreateUser(user)
+ } else {
+ user, err := db.GetUserById(id)
+ if err != nil {
+ http.NotFound(w, r)
+ return
+ }
+ db.SetUserIsAdmin(user.Username, r.Form.Get("isAdmin") == "1")
+ }
+ }
+ http.Redirect(w, r, "/users/", http.StatusFound)
+}
+
+func deleteUser(w http.ResponseWriter, r *http.Request) {
+ err := r.ParseForm()
+ if err == nil {
+ id, err := strconv.Atoi(r.Form.Get("userId"))
+ if err != nil {
+ return
+ }
+ user, err := db.GetUserById(id)
+ if err != nil {
+ http.NotFound(w, r)
+ return
+ }
+ db.DeleteUser(user.Username)
+ }
+ http.Redirect(w, r, "/users/", http.StatusFound)
+}
+
+func resetUserPassword(w http.ResponseWriter, r *http.Request) {
+ err := r.ParseForm()
+ if err == nil {
+ id, err := strconv.Atoi(r.Form.Get("userId"))
+ if err != nil {
+ return
+ }
+ user, err := db.GetUserById(id)
+ if err != nil {
+ http.NotFound(w, r)
+ return
+ }
+ newPassword := r.Form.Get("newPassword")
+ hashed, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
+ if err != nil {
+ return
+ }
+ db.SetUserPassword(user.Username, string(hashed))
}
+ http.Redirect(w, r, "/users/", http.StatusFound)
}
type ChangePasswordPageData struct {
ShowForm bool
}
-func changePasswordPage(w http.ResponseWriter, r *http.Request) {
- user, err := currentUser(w, r)
- if err != nil {
- http.Redirect(w, r, "/login", http.StatusFound)
- return
- }
+func changePasswordPage(w http.ResponseWriter, r *http.Request, user User) {
var data ChangePasswordPageData
if r.Method == "POST" {
err := r.ParseForm()
data.Message = ""
data.ShowForm = true
}
- renderHeader(w, "change-password")
+ renderHeader(w, "change-password", user)
tmpl := template.Must(template.ParseFS(content, "templates/change_password.html"))
- err = tmpl.Execute(w, data)
+ err := tmpl.Execute(w, data)
+ if err != nil {
+ log.Fatal(err)
+ }
+ renderFooter(w)
+}
+
+type UsersPageData struct {
+ Users []User
+}
+
+func usersPage(w http.ResponseWriter, _ *http.Request, user User) {
+ renderHeader(w, "users", user)
+ data := UsersPageData{
+ Users: db.GetUsers(),
+ }
+ tmpl := template.Must(template.ParseFS(content, "templates/users.html"))
+ err := tmpl.Execute(w, data)
if err != nil {
log.Fatal(err)
}
Playlists []Playlist
}
-func playlistsPage(w http.ResponseWriter, _ *http.Request) {
- renderHeader(w, "playlists")
+func playlistsPage(w http.ResponseWriter, _ *http.Request, user User) {
+ renderHeader(w, "playlists", user)
data := PlaylistsPageData{
Playlists: db.GetPlaylists(),
}
Radios []Radio
}
-func radiosPage(w http.ResponseWriter, _ *http.Request) {
- renderHeader(w, "radios")
+func radiosPage(w http.ResponseWriter, _ *http.Request, user User) {
+ renderHeader(w, "radios", user)
data := RadiosPageData{
Radios: db.GetRadios(),
}
Files []string
}
-func editPlaylistPage(w http.ResponseWriter, r *http.Request, id int) {
+func editPlaylistPage(w http.ResponseWriter, r *http.Request, id int, user User) {
var data EditPlaylistPageData
for _, f := range files.Files() {
data.Files = append(data.Files, f.Name)
data.Playlist = playlist
data.Entries = db.GetEntriesForPlaylist(id)
}
- renderHeader(w, "radios")
+ renderHeader(w, "radios", user)
tmpl := template.Must(template.ParseFS(content, "templates/playlist.html"))
tmpl.Execute(w, data)
renderFooter(w)
Radio Radio
}
-func editRadioPage(w http.ResponseWriter, r *http.Request, id int) {
+func editRadioPage(w http.ResponseWriter, r *http.Request, id int, user User) {
var data EditRadioPageData
if id == 0 {
data.Radio.Name = "New Radio"
}
data.Radio = radio
}
- renderHeader(w, "radios")
+ renderHeader(w, "radios", user)
tmpl := template.Must(template.ParseFS(content, "templates/radio.html"))
tmpl.Execute(w, data)
renderFooter(w)
Files []FileSpec
}
-func filesPage(w http.ResponseWriter, _ *http.Request) {
- renderHeader(w, "files")
+func filesPage(w http.ResponseWriter, _ *http.Request, user User) {
+ renderHeader(w, "files", user)
data := FilesPageData{
Files: files.Files(),
}
http.Redirect(w, r, "/files/", http.StatusFound)
}
-func logOutPage(w http.ResponseWriter, r *http.Request) {
+func logOutPage(w http.ResponseWriter, r *http.Request, user User) {
+ cookie, err := r.Cookie("broadcast_session")
+ if err == nil {
+ db.ClearSession(user.Username, cookie.Value)
+ }
clearSessionCookie(w)
- renderHeader(w, "")
+ renderHeader(w, "", user)
tmpl := template.Must(template.ParseFS(content, "templates/logout.html"))
tmpl.Execute(w, nil)
renderFooter(w)
}
-func stopPage(w http.ResponseWriter, r *http.Request) {
- _, err := currentUser(w, r)
- if err != nil {
- http.Redirect(w, r, "/login", http.StatusFound)
- return
- }
+func stopPage(w http.ResponseWriter, r *http.Request, user User) {
r.ParseForm()
radioId, err := strconv.Atoi(r.Form.Get("radioId"))
if err != nil {