Move to git.themmonk.com
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/opal
|
||||||
|
|
||||||
|
#used for testing streaming/playback
|
||||||
|
*.mp4
|
||||||
|
*.mk4
|
||||||
2
api.txt
Normal file
2
api.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
If you actually want me to write some proper docs explaining the api endpoints, submit an issue.
|
||||||
|
In the meantime, read the code under internal/httpserver/api
|
||||||
19
cmd/opal/opal.go
Normal file
19
cmd/opal/opal.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"opal/internal/config"
|
||||||
|
"opal/internal/httpserver"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: use linker version numbers for releases
|
||||||
|
var version_number = "dev"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("Opal media server starting \nVersion:", version_number)
|
||||||
|
|
||||||
|
config.Init()
|
||||||
|
//usermgmt.Init()
|
||||||
|
|
||||||
|
httpserver.Start()
|
||||||
|
}
|
||||||
4
config/libraries/lib1.cfg
Normal file
4
config/libraries/lib1.cfg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
name="Library 1"
|
||||||
|
#do not put spaces or weird chars in path name, numbers allowed
|
||||||
|
path_name="lib1"
|
||||||
|
path="./lib1"
|
||||||
4
config/libraries/lib2.cfg
Normal file
4
config/libraries/lib2.cfg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
name="Library 2"
|
||||||
|
#do not put spaces or weird chars in path name, numbers allowed
|
||||||
|
path_name="lib2"
|
||||||
|
path="./lib2"
|
||||||
2
config/server.cfg
Normal file
2
config/server.cfg
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
address=127.0.0.1
|
||||||
|
port=8096
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module opal
|
||||||
|
|
||||||
|
go 1.25.3
|
||||||
|
|
||||||
|
require github.com/google/uuid v1.6.0 // indirect
|
||||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
161
internal/config/config.go
Normal file
161
internal/config/config.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigNode struct {
|
||||||
|
Name string
|
||||||
|
Children []ConfigNode
|
||||||
|
Properties []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var configDirectoryPath string = "./config"
|
||||||
|
var RootConfigNode ConfigNode
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
//TODO: use flags module instead
|
||||||
|
for _, arg := range os.Args[1:] {
|
||||||
|
if !strings.HasPrefix(arg, "--config-dir=") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dirStrSplit := strings.Split(arg, "=")
|
||||||
|
configDirectoryPath = dirStrSplit[len(dirStrSplit)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Config directory:", configDirectoryPath)
|
||||||
|
RootConfigNode = LoadConfig(configDirectoryPath, "/")
|
||||||
|
PrintConfigTree(RootConfigNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(filePath string, name string) ConfigNode {
|
||||||
|
var retNode ConfigNode
|
||||||
|
retNode.Name = name
|
||||||
|
|
||||||
|
fileStat, err := os.Lstat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Config parsing error: %s\n", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileMode := fileStat.Mode()
|
||||||
|
|
||||||
|
if !fileMode.IsDir() && (filePath == configDirectoryPath) {
|
||||||
|
fmt.Printf("Config parsing error: %s is not a directory\n", configDirectoryPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileMode&os.ModeSymlink != 0 {
|
||||||
|
fmt.Println("Config parsing error: symlinks currently not supported in config tree")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileMode.IsDir() {
|
||||||
|
retNode.Properties = append(retNode.Properties, "folder=1")
|
||||||
|
subDirEntries, err := os.ReadDir(filePath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Config parsing error: %s\n", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, subFile := range subDirEntries {
|
||||||
|
var childName string
|
||||||
|
if filePath == configDirectoryPath {
|
||||||
|
childName = "/" + subFile.Name()
|
||||||
|
} else {
|
||||||
|
childName = name + "/" + subFile.Name()
|
||||||
|
}
|
||||||
|
retNode.Children = append(retNode.Children, LoadConfig(filePath+"/"+subFile.Name(), childName))
|
||||||
|
}
|
||||||
|
return retNode
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fileMode.IsRegular() {
|
||||||
|
fmt.Printf("Config parsing error: unable to determine filetype of %s\n", filePath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileBytes, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Config parsing error: %s\n", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fileString := string(fileBytes)
|
||||||
|
retNode.Properties = strings.Split(fileString, "\n")
|
||||||
|
|
||||||
|
retNode.Properties = slices.DeleteFunc(retNode.Properties, func(property string) bool {
|
||||||
|
return strings.HasPrefix(property, "#")
|
||||||
|
})
|
||||||
|
|
||||||
|
return retNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrintConfigTree(node ConfigNode) {
|
||||||
|
fmt.Printf("%s\n", node.Name)
|
||||||
|
|
||||||
|
if NodeIsFolder(&node) {
|
||||||
|
for _, child := range node.Children {
|
||||||
|
PrintConfigTree(child)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, property := range node.Properties {
|
||||||
|
fmt.Printf("%v: %s\n", i, property)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NodeIsFolder(node *ConfigNode) bool {
|
||||||
|
return slices.Contains(node.Properties, "folder=1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindNode(path string, startNode *ConfigNode) *ConfigNode {
|
||||||
|
if startNode.Name == path {
|
||||||
|
return startNode
|
||||||
|
}
|
||||||
|
|
||||||
|
if !NodeIsFolder(startNode) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range startNode.Children {
|
||||||
|
foundNode := FindNode(path, &child)
|
||||||
|
if foundNode != nil {
|
||||||
|
return foundNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchValue(path string, key string, errorOnFail bool) string {
|
||||||
|
pathNode := FindNode(path, &RootConfigNode)
|
||||||
|
if pathNode != nil {
|
||||||
|
//fmt.Printf("Found node %s @ %p\n", path, pathNode)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Config fetching error: unable to find %s in config directory\n", path)
|
||||||
|
if errorOnFail {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return "ERROR_COULD_NOT_FIND_NODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, property := range pathNode.Properties {
|
||||||
|
if strings.HasPrefix(property, key+"=") {
|
||||||
|
val := strings.SplitN(property, "=", 2)[1]
|
||||||
|
val = strings.TrimSpace(val)
|
||||||
|
val = strings.Trim(val, `"'`)
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errorOnFail {
|
||||||
|
fmt.Printf("Config fetching error: unable to find %s in %s\n", key, configDirectoryPath+path)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return "ERROR_NODE_DOES_NOT_CONTAIN_KEY"
|
||||||
|
}
|
||||||
46
internal/httpserver/api/authenticate.go
Normal file
46
internal/httpserver/api/authenticate.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterAuthEndpoints() {
|
||||||
|
fmt.Println("TODO: change access token format from uuid to a more secure format")
|
||||||
|
|
||||||
|
http.HandleFunc("/auth/user_and_pass", EndpointAuthUserAndPass)
|
||||||
|
http.HandleFunc("/auth/check_access_token", EndpointAuthCheckAccessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
type returnEndpointAuthUserAndPass struct {
|
||||||
|
AccessToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
func EndpointAuthUserAndPass(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Println("STUB WARNING: EndpointAuthUserAndPass, need to check method and implement authentication")
|
||||||
|
|
||||||
|
//gopls doesnt like this line unless prefixed with var, may be a skill issue on my end
|
||||||
|
var ret returnEndpointAuthUserAndPass
|
||||||
|
ret.AccessToken = uuid.New().String()
|
||||||
|
|
||||||
|
retJson, err := json.Marshal(ret)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("API warning: failed to marshal /auth/user_and_pass response")
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(retJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EndpointAuthCheckAccessToken(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Println("STUB WARNING: EndpointAuthCheckToken, need to check method and implement authentication")
|
||||||
|
|
||||||
|
//w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
}
|
||||||
139
internal/httpserver/api/libraries.go
Normal file
139
internal/httpserver/api/libraries.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"opal/internal/config"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterLibraryEndpoints() {
|
||||||
|
ProbeLibraries()
|
||||||
|
|
||||||
|
http.HandleFunc("/libraries", EndpointLibraries)
|
||||||
|
http.HandleFunc("/libraries/", EndpointLibraryList)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Library struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
PathName string `json:"PathName"`
|
||||||
|
Path string `json:"-"` //when marshalled, this will be excluded
|
||||||
|
}
|
||||||
|
|
||||||
|
var Libraries []Library
|
||||||
|
|
||||||
|
type endpointLibraries struct {
|
||||||
|
AvailableLibraries []Library
|
||||||
|
}
|
||||||
|
|
||||||
|
type LibraryListEntry struct {
|
||||||
|
Name string
|
||||||
|
Type string //file, movie, show, directory, may add more filetypes in future
|
||||||
|
}
|
||||||
|
|
||||||
|
type endpointLibraryList struct {
|
||||||
|
LibraryInfo Library
|
||||||
|
Listing []LibraryListEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func EndpointLibraries(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var ret endpointLibraries
|
||||||
|
|
||||||
|
ret.AvailableLibraries = Libraries
|
||||||
|
|
||||||
|
retJson, err := json.Marshal(ret)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("API warning: failed to marshal /libraries response")
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(retJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO/NON-CRITICAL BUG FIX: web client sends /libraries//{path}/ that then redirects to /libraries/{path}/, should put this comment in the web client code but im too lazy
|
||||||
|
func EndpointLibraryList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
url_split := strings.Split(r.URL.Path, "/")
|
||||||
|
if url_split[0] != "" || url_split[1] != "libraries" { //impossible for this to fail unless there is something really wrong
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(url_split) < 4 {
|
||||||
|
http.Error(w, "Malformed request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
library_string := url_split[2]
|
||||||
|
path := strings.Join(url_split[3:], "/")
|
||||||
|
|
||||||
|
var ret endpointLibraryList
|
||||||
|
|
||||||
|
//TODO: use a map instead
|
||||||
|
for _, lib := range Libraries {
|
||||||
|
if lib.PathName != library_string {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret.LibraryInfo = lib
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret.LibraryInfo.Name == "" { //not found in Libraries because string un-initialised
|
||||||
|
http.Error(w, "Malformed request", http.StatusBadRequest) //TODO: library not found (should return 404?)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: this is probably a vulnerability
|
||||||
|
directoryListing, err := os.ReadDir(ret.LibraryInfo.Path + "/" + path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("API warning: potential server error or malicious client: %s\n", err.Error())
|
||||||
|
http.Error(w, "Malformed request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, directoryEntry := range directoryListing {
|
||||||
|
var entry LibraryListEntry
|
||||||
|
|
||||||
|
fmt.Println(directoryEntry.Name())
|
||||||
|
if directoryEntry.IsDir() {
|
||||||
|
entry.Name = directoryEntry.Name()
|
||||||
|
entry.Type = "directory"
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.Listing = append(ret.Listing, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
retJson, err := json.Marshal(ret)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("API warning: failed to marshal library listing response for %s\n", library_string)
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(retJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProbeLibraries() {
|
||||||
|
libraries_config := config.FindNode("/libraries", &config.RootConfigNode)
|
||||||
|
if libraries_config == nil {
|
||||||
|
fmt.Println("Error: unable to find config node for libraries")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range libraries_config.Children {
|
||||||
|
var lib Library
|
||||||
|
lib.Name = config.FetchValue(child.Name, "name", false)
|
||||||
|
lib.PathName = config.FetchValue(child.Name, "path_name", false)
|
||||||
|
lib.Path = config.FetchValue(child.Name, "path", false)
|
||||||
|
|
||||||
|
if strings.HasPrefix("ERROR", lib.Name) || strings.HasPrefix("ERROR", lib.PathName) {
|
||||||
|
fmt.Printf("Error: %s: bad config\n", child.Name)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Libraries = append(Libraries, lib)
|
||||||
|
}
|
||||||
|
}
|
||||||
33
internal/httpserver/api/misc.go
Normal file
33
internal/httpserver/api/misc.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterMiscEndpoints() {
|
||||||
|
http.HandleFunc("/opal", EndpointOpal)
|
||||||
|
}
|
||||||
|
|
||||||
|
type endpointOpalResponse struct {
|
||||||
|
ServerApi string
|
||||||
|
ApiVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /opal
|
||||||
|
func EndpointOpal(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var ret endpointOpalResponse
|
||||||
|
ret.ServerApi = "Opal"
|
||||||
|
ret.ApiVersion = "0.0.1"
|
||||||
|
|
||||||
|
retJson, err := json.Marshal(ret)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("API warning: failed to marshal /opal response")
|
||||||
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(retJson)
|
||||||
|
}
|
||||||
21
internal/httpserver/start.go
Normal file
21
internal/httpserver/start.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package httpserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"opal/internal/config"
|
||||||
|
"opal/internal/httpserver/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Start() {
|
||||||
|
Addr := config.FetchValue("/server.cfg", "address", true)
|
||||||
|
Port := config.FetchValue("/server.cfg", "port", true)
|
||||||
|
|
||||||
|
api.RegisterMiscEndpoints()
|
||||||
|
api.RegisterAuthEndpoints()
|
||||||
|
api.RegisterLibraryEndpoints()
|
||||||
|
|
||||||
|
RegisterWebui()
|
||||||
|
|
||||||
|
http.ListenAndServe(Addr+":"+Port, nil)
|
||||||
|
}
|
||||||
18
internal/httpserver/webui.go
Normal file
18
internal/httpserver/webui.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package httpserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterWebui() {
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/" {
|
||||||
|
http.Redirect(w, r, "/web/", http.StatusPermanentRedirect)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.NotFound(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
fs := http.FileServer(http.Dir("web"))
|
||||||
|
http.Handle("/web/", http.StripPrefix("/web/", fs))
|
||||||
|
}
|
||||||
22
todo.txt
Normal file
22
todo.txt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
TODO: LITERALLY HALF THE API ISNT EVEN AWARE ABOUT AUTHENTICATION, FIX BEFORE RELEASE
|
||||||
|
|
||||||
|
video player, use hls, add direct download at somepoint
|
||||||
|
|
||||||
|
list available files, movies, videos, shows, and folders, for future release, currently just glorified file server
|
||||||
|
upon the selection of a movie or show, display info and play button, for future release
|
||||||
|
|
||||||
|
add hls streaming and support with ffmpeg (server side)
|
||||||
|
|
||||||
|
add library restrictions based on user permissions and details
|
||||||
|
add user groups for mass application of permissions (admin, lib1, lib2, etc)
|
||||||
|
implement auth
|
||||||
|
|
||||||
|
list libraries done, fix with auth
|
||||||
|
|
||||||
|
fix /web/libraries and /web/login to use api js instead of /web/js/login.js and /web/js/libraries.js
|
||||||
|
|
||||||
|
music streaming?
|
||||||
|
electron app for desktop?
|
||||||
|
cli tool for easy bulk downloads?
|
||||||
|
|
||||||
|
temporarily use local cache folder, use /var/tmp for fhs compliance later down the line (make user customisable)
|
||||||
3
web/index.html
Normal file
3
web/index.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<script>
|
||||||
|
window.location.replace('/web/login');
|
||||||
|
</script>
|
||||||
0
web/js/api/auth.js
Normal file
0
web/js/api/auth.js
Normal file
22
web/js/api/libraries.js
Normal file
22
web/js/api/libraries.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import * as auth from '/web/js/api/auth.js';
|
||||||
|
import ky from 'https://cdn.jsdelivr.net/npm/ky@1.14.1/+esm'
|
||||||
|
|
||||||
|
export async function ls(library, path) {
|
||||||
|
try {
|
||||||
|
const response = await ky.get(`/libraries/${library}/${path}`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${auth.ACCESS_TOKEN}`,
|
||||||
|
},
|
||||||
|
}).json();
|
||||||
|
|
||||||
|
return response;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
if (err.response) {
|
||||||
|
console.error('Unexpected HTTP status code', err.response.status);
|
||||||
|
} else {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
return "err";
|
||||||
|
}
|
||||||
|
}
|
||||||
69
web/js/auth.js
Normal file
69
web/js/auth.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
const accessToken = localStorage.getItem('accessToken');
|
||||||
|
|
||||||
|
async function isAccessTokenOkay() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/auth/check_access_token', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) return true;
|
||||||
|
if (response.status === 401) return false;
|
||||||
|
|
||||||
|
console.error('Error: server sent unexpected HTTP status code', response.status);
|
||||||
|
return false;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Network or server error:', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
if (!accessToken) return;
|
||||||
|
|
||||||
|
const valid = await isAccessTokenOkay();
|
||||||
|
if (valid && window.location.pathname === '/web/login') {
|
||||||
|
window.location.href = '/web/libraries';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
window.location.href = '/web/login';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
//used in /web/login, not a general purpose function
|
||||||
|
async function getAccessToken() {
|
||||||
|
const username = document.getElementById('username').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/auth/user_and_pass', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
const data = await response.json();
|
||||||
|
const accessToken = data.AccessToken;
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
alert('Login failed: no token returned');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('accessToken', accessToken);
|
||||||
|
window.location.href = '/web/libraries';
|
||||||
|
} else if (response.status === 401) {
|
||||||
|
alert('Invalid username or password');
|
||||||
|
} else {
|
||||||
|
alert('Server error, please try again later.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert('Network error, please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
49
web/js/libraries.js
Normal file
49
web/js/libraries.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
//auth.js handles accessToken
|
||||||
|
//const accessToken = localStorage.getItem("accessToken");
|
||||||
|
|
||||||
|
//only to be used by /web/libraries, not general purpose
|
||||||
|
|
||||||
|
async function getAllLibraries() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/libraries', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
alert("Error: access forbidden, please reauthenticate if error persists");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.error('Unexpected HTTP status code', response.status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const libraries = data.AvailableLibraries;
|
||||||
|
|
||||||
|
const container = document.getElementById("library-entry-container");
|
||||||
|
const template = document.getElementById("library-template");
|
||||||
|
|
||||||
|
container.innerHTML = "";
|
||||||
|
|
||||||
|
libraries.forEach(lib => {
|
||||||
|
const clone = template.content.cloneNode(true);
|
||||||
|
const link = clone.querySelector(".library-viewer-href");
|
||||||
|
|
||||||
|
link.textContent = lib.Name;
|
||||||
|
link.href = `/web/library_viewer?lib=${encodeURIComponent(lib.PathName)}&path=/`;
|
||||||
|
|
||||||
|
container.appendChild(clone);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
alert("Network or server error");
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllLibraries();
|
||||||
3
web/js/pages/library_viewer.js
Normal file
3
web/js/pages/library_viewer.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import * as lib from '/web/js/api/libraries.js';
|
||||||
|
|
||||||
|
console.log(await lib.ls("lib1", "/"))
|
||||||
24
web/libraries
Normal file
24
web/libraries
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Opal Media Server</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Libraries</h2>
|
||||||
|
|
||||||
|
<div id="library-entry-container"></div>
|
||||||
|
|
||||||
|
<!-- begin templates -->
|
||||||
|
<template id="library-template">
|
||||||
|
<a class="library-viewer-href"></a>
|
||||||
|
</template>
|
||||||
|
<!-- end templates -->
|
||||||
|
|
||||||
|
<!-- begin js -->
|
||||||
|
<script src="/web/js/auth.js" defer></script>
|
||||||
|
<script src="/web/js/libraries.js" defer></script>
|
||||||
|
<!-- end js -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
18
web/library_viewer
Normal file
18
web/library_viewer
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Opal Media Server</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2 id="library-viewer-header">Library viewer:</h2>
|
||||||
|
|
||||||
|
<!-- begin templates -->
|
||||||
|
<!-- end templates -->
|
||||||
|
|
||||||
|
<!-- begin js -->
|
||||||
|
<script type="module" src="/web/js/pages/library_viewer.js"></script>
|
||||||
|
<!-- end js -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
30
web/login
Normal file
30
web/login
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Opal Media Server</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Login</h2>
|
||||||
|
<form id="loginForm">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input type="text" id="username" name="username" required><br><br>
|
||||||
|
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" name="password" required><br><br>
|
||||||
|
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script src="/web/js/auth.js"></script>
|
||||||
|
<script>
|
||||||
|
const loginForm = document.getElementById('loginForm');
|
||||||
|
|
||||||
|
loginForm.addEventListener('submit', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
getAccessToken();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user