feat: bundle frontend with API executable
This commit is contained in:
		
							parent
							
								
									57c7a0f4de
								
							
						
					
					
						commit
						6c9ebf1d08
					
				
					 13 changed files with 105 additions and 16 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -10,8 +10,9 @@ pnpm-debug.log* | |||
| lerna-debug.log* | ||||
| 
 | ||||
| node_modules | ||||
| dist | ||||
| frontend/dist/* | ||||
| dist-ssr | ||||
| !frontend/dist/.empty | ||||
| *.local | ||||
| 
 | ||||
| # Editor directories and files | ||||
|  |  | |||
							
								
								
									
										6
									
								
								Makefile
									
										
									
									
									
								
							
							
						
						
									
										6
									
								
								Makefile
									
										
									
									
									
								
							|  | @ -1,10 +1,14 @@ | |||
| .PHONY: all | ||||
| all: frontend backend | ||||
| 	mv api pronouns | ||||
| 
 | ||||
| .PHONY: migrate | ||||
| migrate: | ||||
| 	go run -v ./scripts/migrate | ||||
| 
 | ||||
| .PHONY: backend | ||||
| backend: | ||||
| 	go build -v -o api -ldflags="-buildid= -X codeberg.org/u1f320/pronouns.cc/backend/server.Revision=`git rev-parse --short HEAD`" ./backend | ||||
| 	CGO_ENABLED=0 go build -v -o api -ldflags="-buildid= -X codeberg.org/u1f320/pronouns.cc/backend/server.Revision=`git rev-parse --short HEAD`" ./backend | ||||
| 
 | ||||
| .PHONY: frontend | ||||
| frontend: | ||||
|  |  | |||
							
								
								
									
										11
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								README.md
									
										
									
									
									
								
							|  | @ -9,6 +9,17 @@ A work-in-progress site to share your pronouns and preferred terms. | |||
| - Temporary data is stored in Redis | ||||
| - The frontend is written in TypeScript with React, using [Vite](https://vitejs.dev/) | ||||
| 
 | ||||
| ## Development | ||||
| 
 | ||||
| Note that | ||||
| 
 | ||||
| ## Building | ||||
| 
 | ||||
| Run `make all`. This will build the frontend, then embed that in the backend. | ||||
| 
 | ||||
| The resulting `pronouns` binary is a statically linked executable containing everything needed to run the website.   | ||||
| Note that it should still be run behind a reverse proxy for TLS. | ||||
| 
 | ||||
| ## License | ||||
| 
 | ||||
|     Copyright (C) 2022  Sam <u1f320> | ||||
|  |  | |||
|  | @ -2,13 +2,18 @@ package main | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"codeberg.org/u1f320/pronouns.cc/backend/log" | ||||
| 	"codeberg.org/u1f320/pronouns.cc/backend/server" | ||||
| 	"codeberg.org/u1f320/pronouns.cc/frontend" | ||||
| 
 | ||||
| 	"github.com/go-chi/chi/v5" | ||||
| 	"github.com/go-chi/chi/v5/middleware" | ||||
| 	_ "github.com/joho/godotenv/autoload" | ||||
| ) | ||||
| 
 | ||||
|  | @ -23,11 +28,14 @@ func main() { | |||
| 	// mount api routes | ||||
| 	mountRoutes(s) | ||||
| 
 | ||||
| 	r := chi.NewMux() | ||||
| 	setupFrontend(r, s) | ||||
| 
 | ||||
| 	e := make(chan error) | ||||
| 
 | ||||
| 	// run server in another goroutine (for gracefully shutting down, see below) | ||||
| 	go func() { | ||||
| 		e <- http.ListenAndServe(port, s.Router) | ||||
| 		e <- http.ListenAndServe(port, r) | ||||
| 	}() | ||||
| 
 | ||||
| 	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) | ||||
|  | @ -44,3 +52,44 @@ func main() { | |||
| 		log.Fatalf("Error running server: %v", err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func setupFrontend(r chi.Router, s *server.Server) { | ||||
| 	r.Use(middleware.Recoverer) | ||||
| 
 | ||||
| 	r.Get("/@{user}", a) | ||||
| 	r.Get("/@{user}/{member}", a) | ||||
| 
 | ||||
| 	r.Mount("/api", s.Router) | ||||
| 
 | ||||
| 	r.NotFound(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		var ( | ||||
| 			data []byte | ||||
| 			err  error | ||||
| 		) | ||||
| 
 | ||||
| 		if strings.HasSuffix(r.URL.Path, ".js") { | ||||
| 			data, err = frontend.Data.ReadFile("dist" + r.URL.Path) | ||||
| 			w.Header().Add("content-type", "application/javascript") | ||||
| 		} else if strings.HasSuffix(r.URL.Path, ".css") { | ||||
| 			data, err = frontend.Data.ReadFile("dist" + r.URL.Path) | ||||
| 			w.Header().Add("content-type", "text/css") | ||||
| 		} else if strings.HasSuffix(r.URL.Path, ".map") { | ||||
| 			data, err = frontend.Data.ReadFile("dist" + r.URL.Path) | ||||
| 		} else { | ||||
| 			data, err = frontend.Data.ReadFile("dist/index.html") | ||||
| 			w.Header().Add("content-type", "text/html") | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			panic(err) | ||||
| 		} | ||||
| 
 | ||||
| 		w.Write(data) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func a(w http.ResponseWriter, r *http.Request) { | ||||
| 	user := chi.URLParam(r, "user") | ||||
| 	member := chi.URLParam(r, "member") | ||||
| 
 | ||||
| 	fmt.Fprintf(w, "user: %v, member: %v", user, member) | ||||
| } | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| package auth | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 
 | ||||
|  | @ -85,13 +84,13 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error { | |||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		fmt.Println(token) | ||||
| 
 | ||||
| 		render.JSON(w, r, discordCallbackResponse{ | ||||
| 			HasAccount: true, | ||||
| 			Token:      token, | ||||
| 			User:       &u, | ||||
| 		}) | ||||
| 		return nil | ||||
| 
 | ||||
| 	} else if err != db.ErrUserNotFound { // internal error | ||||
| 		return err | ||||
| 	} | ||||
|  |  | |||
							
								
								
									
										6
									
								
								frontend/data.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/data.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| package frontend | ||||
| 
 | ||||
| import "embed" | ||||
| 
 | ||||
| //go:embed dist/* | ||||
| var Data embed.FS | ||||
|  | @ -1,4 +1,4 @@ | |||
| import { Routes, Route, useParams } from "react-router-dom"; | ||||
| import { Routes, Route } from "react-router-dom"; | ||||
| import "./App.css"; | ||||
| import Container from "./lib/Container"; | ||||
| import Navigation from "./lib/Navigation"; | ||||
|  | @ -15,8 +15,8 @@ function App() { | |||
|       <Container> | ||||
|         <Routes> | ||||
|           <Route path="/" element={<Home />} /> | ||||
|           <Route path="/@:username" element={<User />} /> | ||||
|           <Route path="/@:username/:member" element={<User />} /> | ||||
|           <Route path="/u/:username" element={<User />} /> | ||||
|           <Route path="/u/:username/:member" element={<User />} /> | ||||
|           <Route path="/edit" element={<EditMe />} /> | ||||
|           <Route path="/edit/:member" element={<EditMe />} /> | ||||
|           <Route path="/login" element={<Login />} /> | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ import Logo from "./logo"; | |||
| import { useRecoilState } from "recoil"; | ||||
| import { userState } from "./store"; | ||||
| import fetchAPI from "./fetch"; | ||||
| import { MeUser } from "./types"; | ||||
| import { APIError, ErrorCode, MeUser } from "./types"; | ||||
| 
 | ||||
| function Navigation() { | ||||
|   const [user, setUser] = useRecoilState(userState); | ||||
|  | @ -17,7 +17,15 @@ function Navigation() { | |||
| 
 | ||||
|     fetchAPI<MeUser>("/users/@me").then( | ||||
|       (res) => setUser(res), | ||||
|       (err) => console.log("fetching /users/@me", err) | ||||
|       (err) => { | ||||
|         console.log("fetching /users/@me", err); | ||||
|         if ( | ||||
|           (err as APIError).code == ErrorCode.InvalidToken || | ||||
|           (err as APIError).code == ErrorCode.Forbidden | ||||
|         ) { | ||||
|           localStorage.removeItem("pronouns-token"); | ||||
|         } | ||||
|       } | ||||
|     ); | ||||
|   }, []); | ||||
| 
 | ||||
|  | @ -45,7 +53,7 @@ function Navigation() { | |||
| 
 | ||||
|   const nav = user ? ( | ||||
|     <> | ||||
|       <NavItem to={`/@${user.username}`}>@{user.username}</NavItem> | ||||
|       <NavItem to={`/u/${user.username}`}>@{user.username}</NavItem> | ||||
|       <NavItem to="/settings">Settings</NavItem> | ||||
|       <NavItem to="/logout">Log out</NavItem> | ||||
|     </> | ||||
|  |  | |||
|  | @ -6,10 +6,18 @@ export default async function fetchAPI<T>( | |||
|   method = "GET", | ||||
|   body = null | ||||
| ) { | ||||
|   let headers = {}; | ||||
|   const token = localStorage.getItem("pronouns-token"); | ||||
|   if (token) { | ||||
|     headers = { | ||||
|       Authorization: token, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   const resp = await fetch(`/api/v1${path}`, { | ||||
|     method, | ||||
|     headers: { | ||||
|       Authorization: localStorage.getItem("pronouns-token"), | ||||
|       ...headers, | ||||
|       "Content-Type": "application/json", | ||||
|     }, | ||||
|     body: body ? JSON.stringify(body) : null, | ||||
|  |  | |||
|  | @ -15,7 +15,10 @@ async function getCurrentUser() { | |||
|   try { | ||||
|     return await fetchAPI<MeUser>("/users/@me"); | ||||
|   } catch (e) { | ||||
|     if ((e as APIError).code === ErrorCode.Forbidden) { | ||||
|     if ( | ||||
|       (e as APIError).code === ErrorCode.Forbidden || | ||||
|       (e as APIError).code === ErrorCode.InvalidToken | ||||
|     ) { | ||||
|       localStorage.removeItem("pronouns-token"); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -43,6 +43,7 @@ export enum ErrorCode { | |||
| 
 | ||||
|   InvalidState = 1001, | ||||
|   InvalidOAuthCode = 1002, | ||||
|   InvalidToken = 1003, | ||||
| 
 | ||||
|   UserNotFound = 2001, | ||||
| } | ||||
|  |  | |||
|  | @ -55,7 +55,7 @@ export default function Discord() { | |||
|         console.log("token:", resp.token); | ||||
|         localStorage.setItem("pronouns-token", resp.token); | ||||
| 
 | ||||
|         if (resp.user) setUser(resp.user as MeUser); | ||||
|         setUser(resp.user); | ||||
|       }, | ||||
|       (err) => { | ||||
|         console.log(err); | ||||
|  |  | |||
|  | @ -11,7 +11,6 @@ export default defineConfig({ | |||
|         // assumes port 8080 in .env for development
 | ||||
|         target: "http://localhost:8080", | ||||
|         changeOrigin: true, | ||||
|         rewrite: (path) => path.replace(/^\/api/, ""), | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue