diff --git a/.dockerignore b/.dockerignore index 4ac60943..ea93f80b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,8 @@ .gitignore .prettierrc .vscode +.env +example.env photos_path screenshots diff --git a/Dockerfile b/Dockerfile index 93c9f06b..6b1c793b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # Build UI FROM node:10 as ui -ARG GRAPHQL_ENDPOINT -ENV GRAPHQL_ENDPOINT=${GRAPHQL_ENDPOINT} +ARG API_ENDPOINT +ENV API_ENDPOINT=${API_ENDPOINT} RUN mkdir -p /app WORKDIR /app diff --git a/api/example.env b/api/example.env index 29855793..86b0e785 100644 --- a/api/example.env +++ b/api/example.env @@ -2,9 +2,17 @@ MYSQL_URL=user:password@tcp(localhost)/dbname -API_ENDPOINT=http://localhost:4001/ +API_LISTEN_IP=localhost +API_LISTEN_PORT=4001 -PUBLIC_ENDPOINT=http://localhost:1234/ +# The url from which the server can be accessed publicly +API_ENDPOINT=http://localhost:4001/ +UI_ENDPOINT=http://localhost:1234/ + +# Set to 1 for the server to also serve the built static ui files +SERVE_UI=0 +# When SERVE_UI is 1, PUBLIC_ENDPOINT is used instead of API_ENDPOINT and UI_ENDPOINT +#PUBLIC_ENDPOINT=http://localhost:4001/ # Set to 1 to set server in development mode, this enables graphql playground # Remove this if running in production diff --git a/api/graphql/models/photo.go b/api/graphql/models/photo.go index a6492331..dc6eb24c 100644 --- a/api/graphql/models/photo.go +++ b/api/graphql/models/photo.go @@ -2,10 +2,9 @@ package models import ( "database/sql" - "log" - "net/url" - "os" "path" + + "github.com/viktorstrate/photoview/api/utils" ) type Photo struct { @@ -66,18 +65,9 @@ func NewPhotosFromRows(rows *sql.Rows) ([]*Photo, error) { func (p *PhotoURL) URL() string { - publicUrl := os.Getenv("PUBLIC_ENDPOINT") - if publicUrl == "" { - publicUrl = os.Getenv("API_ENDPOINT") - } + imageUrl := utils.ApiEndpointUrl() + imageUrl.Path = path.Join(imageUrl.Path, "photo", p.PhotoName) - imageUrl, err := url.Parse(publicUrl) - if err != nil { - log.Println("Endpoint url is not properly configured, make sure the PUBLIC_ENDPOINT AND API_ENDPOINT environment variables are set correctly") - return p.PhotoName - } - - imageUrl.Path = path.Join(imageUrl.Path, "api", "photo", p.PhotoName) return imageUrl.String() } diff --git a/api/server.go b/api/server.go index 04bca0ab..b0e8310a 100644 --- a/api/server.go +++ b/api/server.go @@ -3,7 +3,6 @@ package main import ( "log" "net/http" - "net/url" "os" "path" "path/filepath" @@ -16,14 +15,13 @@ import ( "github.com/viktorstrate/photoview/api/graphql/auth" "github.com/viktorstrate/photoview/api/routes" "github.com/viktorstrate/photoview/api/server" + "github.com/viktorstrate/photoview/api/utils" "github.com/99designs/gqlgen/handler" photoview_graphql "github.com/viktorstrate/photoview/api/graphql" "github.com/viktorstrate/photoview/api/graphql/resolvers" ) -const defaultPort = "4001" - // spaHandler implements the http.Handler interface, so we can use it // to respond to HTTP requests. The path to the static directory and // path to the index file within that static directory are used to @@ -98,23 +96,19 @@ func main() { Directives: graphqlDirective, } - endpointURL, err := url.Parse(os.Getenv("API_ENDPOINT")) - if err != nil { - log.Println("WARN: Environment variable API_ENDPOINT not specified") - endpointURL, _ = url.Parse("/") - } + apiListenUrl := utils.ApiListenUrl() - endpointRouter := rootRouter.PathPrefix(endpointURL.Path).Subrouter() + endpointRouter := rootRouter.PathPrefix(apiListenUrl.Path).Subrouter() if devMode { - endpointRouter.Handle("/api", handler.Playground("GraphQL playground", path.Join(endpointURL.Path, "/graphql"))) + endpointRouter.Handle("/", handler.Playground("GraphQL playground", path.Join(apiListenUrl.Path, "/graphql"))) } else { - endpointRouter.HandleFunc("/api", func(w http.ResponseWriter, req *http.Request) { + endpointRouter.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("photoview api endpoint")) }) } - endpointRouter.Handle("/api/graphql", + endpointRouter.Handle("/graphql", handler.GraphQL(photoview_graphql.NewExecutableSchema(graphqlConfig), handler.IntrospectionEnabled(devMode), handler.WebsocketUpgrader(server.WebsocketUpgrader(devMode)), @@ -122,22 +116,28 @@ func main() { ), ) - photoRouter := endpointRouter.PathPrefix("/api/photo").Subrouter() + photoRouter := endpointRouter.PathPrefix("/photo").Subrouter() routes.RegisterPhotoRoutes(db, photoRouter) - spa := spaHandler{staticPath: "/ui", indexPath: "index.html"} - endpointRouter.PathPrefix("/").Handler(spa) + shouldServeUI := os.Getenv("SERVE_UI") == "1" - if devMode { - log.Printf("🚀 Graphql playground ready at %s\n", endpointURL.String()) - } else { - log.Printf("Photoview API endpoint listening at %s\n", endpointURL.String()) - - publicEndpoint := os.Getenv("PUBLIC_ENDPOINT") - if publicEndpoint != "" && publicEndpoint != endpointURL.String() { - log.Printf("Photoview API public endpoint ready at %s\n", publicEndpoint) - } + if shouldServeUI { + spa := spaHandler{staticPath: "/ui", indexPath: "index.html"} + rootRouter.PathPrefix("/").Handler(spa) } - log.Fatal(http.ListenAndServe(":"+endpointURL.Port(), rootRouter)) + if devMode { + log.Printf("🚀 Graphql playground ready at %s\n", apiListenUrl.String()) + } else { + log.Printf("Photoview API endpoint listening at %s\n", apiListenUrl.String()) + + uiEndpoint := utils.UiEndpointUrl() + apiEndpoint := utils.ApiEndpointUrl() + + log.Printf("Photoview API public endpoint ready at %s\n", apiEndpoint.String()) + log.Printf("Photoview UI public endpoint ready at %s\n", uiEndpoint.String()) + + } + + log.Fatal(http.ListenAndServe(":"+apiListenUrl.Port(), rootRouter)) } diff --git a/api/server/cors_middleware.go b/api/server/cors_middleware.go index e131b0d5..37e185d2 100644 --- a/api/server/cors_middleware.go +++ b/api/server/cors_middleware.go @@ -1,14 +1,12 @@ package server import ( - "log" "net/http" - "net/url" - "os" "path" "strings" "github.com/gorilla/mux" + "github.com/viktorstrate/photoview/api/utils" ) func CORSMiddleware(devMode bool) mux.MiddlewareFunc { @@ -22,10 +20,7 @@ func CORSMiddleware(devMode bool) mux.MiddlewareFunc { w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ",")) w.Header().Set("Access-Control-Expose-Headers", "content-length") - endpoint, err := url.Parse(os.Getenv("API_ENDPOINT")) - if err != nil { - log.Fatalln("Could not parse API_ENDPOINT environment variable as url") - } + endpoint := utils.ApiEndpointUrl() endpoint.Path = path.Join(endpoint.Path, "graphql") if devMode { @@ -34,12 +29,8 @@ func CORSMiddleware(devMode bool) mux.MiddlewareFunc { w.Header().Set("Vary", "Origin") } else { // Production environment - publicEndpoint, err := url.Parse(os.Getenv("PUBLIC_ENDPOINT")) - if err != nil { - log.Printf("Error parsing environment variable PUBLIC_ENDPOINT as url: %s", err) - } else { - w.Header().Set("Access-Control-Allow-Origin", publicEndpoint.Scheme+"://"+publicEndpoint.Host) - } + uiEndpoint := utils.UiEndpointUrl() + w.Header().Set("Access-Control-Allow-Origin", uiEndpoint.Scheme+"://"+uiEndpoint.Host) } if req.Method != http.MethodOptions { diff --git a/api/server/websocket.go b/api/server/websocket.go index 99043b49..3786d368 100644 --- a/api/server/websocket.go +++ b/api/server/websocket.go @@ -4,9 +4,9 @@ import ( "log" "net/http" "net/url" - "os" "github.com/gorilla/websocket" + "github.com/viktorstrate/photoview/api/utils" ) func WebsocketUpgrader(devMode bool) websocket.Upgrader { @@ -15,11 +15,7 @@ func WebsocketUpgrader(devMode bool) websocket.Upgrader { if devMode { return true } else { - pubEndpoint, err := url.Parse(os.Getenv("PUBLIC_ENDPOINT")) - if err != nil { - log.Printf("Could not parse API_ENDPOINT environment variable as url: %s", err) - return false - } + uiEndpoint := utils.UiEndpointUrl() if r.Header.Get("origin") == "" { return true @@ -31,10 +27,10 @@ func WebsocketUpgrader(devMode bool) websocket.Upgrader { return false } - if pubEndpoint.Host == originURL.Host { + if uiEndpoint.Host == originURL.Host { return true } else { - log.Printf("Not allowing websocket request from %s because it doesn't match PUBLIC_ENDPOINT %s", originURL.Host, pubEndpoint.Host) + log.Printf("Not allowing websocket request from %s because it doesn't match UI_ENDPOINT %s", originURL.Host, uiEndpoint.Host) return false } } diff --git a/api/utils/Endpoints.go b/api/utils/Endpoints.go new file mode 100644 index 00000000..2179a6ab --- /dev/null +++ b/api/utils/Endpoints.go @@ -0,0 +1,82 @@ +package utils + +import ( + "fmt" + "log" + "net/url" + "os" + "path" + "strconv" +) + +func ApiListenUrl() *url.URL { + const defaultPort = "4001" + + shouldServeUI := os.Getenv("SERVE_UI") == "1" + + apiPrefix := "/" + if shouldServeUI { + apiPrefix = "/api" + } + + var listenAddr string + + listenAddr = os.Getenv("API_LISTEN_IP") + if listenAddr == "" { + listenAddr = "127.0.0.1" + } + + listenPortStr := os.Getenv("API_LISTEN_PORT") + if listenPortStr == "" { + listenPortStr = defaultPort + } + + listenPort, err := strconv.Atoi(listenPortStr) + if err != nil { + log.Fatalf("API_LISTEN_PORT must be a number: '%s'\n%s", listenPortStr, err) + } + + apiUrl, err := url.Parse(fmt.Sprintf("http://%s:%d", listenAddr, listenPort)) + if err != nil { + log.Fatalf("Could not format api url: %s", err) + } + apiUrl.Path = apiPrefix + + return apiUrl +} + +func ApiEndpointUrl() *url.URL { + apiEndpointStr := os.Getenv("API_ENDPOINT") + + shouldServeUI := os.Getenv("SERVE_UI") == "1" + if shouldServeUI { + apiEndpointStr = os.Getenv("PUBLIC_ENDPOINT") + } + + apiEndpointUrl, err := url.Parse(apiEndpointStr) + if err != nil { + log.Fatalf("ERROR: Environment variable API_ENDPOINT is not a proper url") + } + + if shouldServeUI { + apiEndpointUrl.Path = path.Join(apiEndpointUrl.Path, "/api") + } + + return apiEndpointUrl +} + +func UiEndpointUrl() *url.URL { + uiEndpointStr := os.Getenv("UI_ENDPOINT") + + shouldServeUI := os.Getenv("SERVE_UI") == "1" + if shouldServeUI { + uiEndpointStr = os.Getenv("PUBLIC_ENDPOINT") + } + + uiEndpointUrl, err := url.Parse(uiEndpointStr) + if err != nil { + log.Fatalf("ERROR: Environment variable UI_ENDPOINT is not a proper url") + } + + return uiEndpointUrl +} diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 9d9739e0..a3278785 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -16,7 +16,7 @@ services: build: context: "." args: - - GRAPHQL_ENDPOINT=http://localhost:8000/api/graphql + - API_ENDPOINT=http://localhost:8000/api/ restart: always ports: @@ -26,21 +26,24 @@ services: environment: - MYSQL_URL=photoview:photo-secret@tcp(db)/photoview + - API_LISTEN_IP=photoview + - API_LISTEN_PORT=80 - PHOTO_CACHE=/app/cache + - SERVE_UI=1 + # Change This: The publicly exposed url for the api # For example if the server is available from the domain example.com, - # change this value to http://example.com/api - - API_ENDPOINT=http://localhost:80/ + # change this value to http://example.com/ - PUBLIC_ENDPOINT=http://localhost:8000/ volumes: - # Change This: Link photo paths from the host machine + - api_cache:/app/cache + # Change this to the directory where your photos are located on your server. # If the photos are located at `/home/user/photos`, then change this value # to the following: `/home/user/photos:/photos:ro`. # You can mount multiple paths, if your photos are spread across multiple directories. - ./photos_path:/photos:ro - - api_cache:/app/cache volumes: db_data: diff --git a/ui/example.env b/ui/example.env index e68a1226..b6a6f46a 100644 --- a/ui/example.env +++ b/ui/example.env @@ -1 +1 @@ -GRAPHQL_ENDPOINT=http://localhost:4001/graphql \ No newline at end of file +API_ENDPOINT=http://localhost:4001/ \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json index 60f5f47b..b80d16bd 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -10996,6 +10996,11 @@ } } }, + "url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", diff --git a/ui/package.json b/ui/package.json index 04ac9bcf..2dabd8b5 100644 --- a/ui/package.json +++ b/ui/package.json @@ -30,7 +30,8 @@ "semantic-ui-css": "^2.4.1", "semantic-ui-react": "^0.88.0", "styled-components": "^5.0.1", - "subscriptions-transport-ws": "^0.9.16" + "subscriptions-transport-ws": "^0.9.16", + "url-join": "^4.0.1" }, "scripts": { "start": "parcel start src/index.html", diff --git a/ui/src/apolloClient.js b/ui/src/apolloClient.js index d8b73c63..6393ffae 100644 --- a/ui/src/apolloClient.js +++ b/ui/src/apolloClient.js @@ -7,17 +7,20 @@ import { setContext } from 'apollo-link-context' import { ApolloLink, split } from 'apollo-link' import { getMainDefinition } from 'apollo-utilities' import { MessageState } from './components/messages/Messages' +import urlJoin from 'url-join' + +const GRAPHQL_ENDPOINT = urlJoin(process.env.API_ENDPOINT, '/graphql') const httpLink = new HttpLink({ - uri: process.env.GRAPHQL_ENDPOINT, + uri: GRAPHQL_ENDPOINT, credentials: 'same-origin', }) -console.log('GRAPHQL ENDPOINT', process.env.GRAPHQL_ENDPOINT) +console.log('API ENDPOINT', process.env.API_ENDPOINT) -const apiProtocol = new URL(process.env.GRAPHQL_ENDPOINT).protocol +const apiProtocol = new URL(process.env.API_ENDPOINT).protocol -let websocketUri = new URL(process.env.GRAPHQL_ENDPOINT) +let websocketUri = new URL(GRAPHQL_ENDPOINT) websocketUri.protocol = apiProtocol === 'https:' ? 'wss:' : 'ws:' const wsLink = new WebSocketLink({