feat: bundle frontend with API executable

This commit is contained in:
Sam 2022-06-17 00:00:52 +02:00
parent 57c7a0f4de
commit 6c9ebf1d08
13 changed files with 105 additions and 16 deletions

3
.gitignore vendored
View file

@ -10,8 +10,9 @@ pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules node_modules
dist frontend/dist/*
dist-ssr dist-ssr
!frontend/dist/.empty
*.local *.local
# Editor directories and files # Editor directories and files

View file

@ -1,10 +1,14 @@
.PHONY: all
all: frontend backend
mv api pronouns
.PHONY: migrate .PHONY: migrate
migrate: migrate:
go run -v ./scripts/migrate go run -v ./scripts/migrate
.PHONY: backend .PHONY: backend
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 .PHONY: frontend
frontend: frontend:

View file

@ -9,6 +9,17 @@ A work-in-progress site to share your pronouns and preferred terms.
- Temporary data is stored in Redis - Temporary data is stored in Redis
- The frontend is written in TypeScript with React, using [Vite](https://vitejs.dev/) - 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 ## License
Copyright (C) 2022 Sam <u1f320> Copyright (C) 2022 Sam <u1f320>

View file

@ -2,13 +2,18 @@ package main
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"strings"
"codeberg.org/u1f320/pronouns.cc/backend/log" "codeberg.org/u1f320/pronouns.cc/backend/log"
"codeberg.org/u1f320/pronouns.cc/backend/server" "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" _ "github.com/joho/godotenv/autoload"
) )
@ -23,11 +28,14 @@ func main() {
// mount api routes // mount api routes
mountRoutes(s) mountRoutes(s)
r := chi.NewMux()
setupFrontend(r, s)
e := make(chan error) e := make(chan error)
// run server in another goroutine (for gracefully shutting down, see below) // run server in another goroutine (for gracefully shutting down, see below)
go func() { go func() {
e <- http.ListenAndServe(port, s.Router) e <- http.ListenAndServe(port, r)
}() }()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
@ -44,3 +52,44 @@ func main() {
log.Fatalf("Error running server: %v", err) 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)
}

View file

@ -1,7 +1,6 @@
package auth package auth
import ( import (
"fmt"
"net/http" "net/http"
"os" "os"
@ -85,13 +84,13 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
return err return err
} }
fmt.Println(token)
render.JSON(w, r, discordCallbackResponse{ render.JSON(w, r, discordCallbackResponse{
HasAccount: true, HasAccount: true,
Token: token, Token: token,
User: &u, User: &u,
}) })
return nil
} else if err != db.ErrUserNotFound { // internal error } else if err != db.ErrUserNotFound { // internal error
return err return err
} }

6
frontend/data.go Normal file
View file

@ -0,0 +1,6 @@
package frontend
import "embed"
//go:embed dist/*
var Data embed.FS

View file

@ -1,4 +1,4 @@
import { Routes, Route, useParams } from "react-router-dom"; import { Routes, Route } from "react-router-dom";
import "./App.css"; import "./App.css";
import Container from "./lib/Container"; import Container from "./lib/Container";
import Navigation from "./lib/Navigation"; import Navigation from "./lib/Navigation";
@ -15,8 +15,8 @@ function App() {
<Container> <Container>
<Routes> <Routes>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />
<Route path="/@:username" element={<User />} /> <Route path="/u/:username" element={<User />} />
<Route path="/@:username/:member" element={<User />} /> <Route path="/u/:username/:member" element={<User />} />
<Route path="/edit" element={<EditMe />} /> <Route path="/edit" element={<EditMe />} />
<Route path="/edit/:member" element={<EditMe />} /> <Route path="/edit/:member" element={<EditMe />} />
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />

View file

@ -7,7 +7,7 @@ import Logo from "./logo";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { userState } from "./store"; import { userState } from "./store";
import fetchAPI from "./fetch"; import fetchAPI from "./fetch";
import { MeUser } from "./types"; import { APIError, ErrorCode, MeUser } from "./types";
function Navigation() { function Navigation() {
const [user, setUser] = useRecoilState(userState); const [user, setUser] = useRecoilState(userState);
@ -17,7 +17,15 @@ function Navigation() {
fetchAPI<MeUser>("/users/@me").then( fetchAPI<MeUser>("/users/@me").then(
(res) => setUser(res), (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 ? ( 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="/settings">Settings</NavItem>
<NavItem to="/logout">Log out</NavItem> <NavItem to="/logout">Log out</NavItem>
</> </>

View file

@ -6,10 +6,18 @@ export default async function fetchAPI<T>(
method = "GET", method = "GET",
body = null body = null
) { ) {
let headers = {};
const token = localStorage.getItem("pronouns-token");
if (token) {
headers = {
Authorization: token,
};
}
const resp = await fetch(`/api/v1${path}`, { const resp = await fetch(`/api/v1${path}`, {
method, method,
headers: { headers: {
Authorization: localStorage.getItem("pronouns-token"), ...headers,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: body ? JSON.stringify(body) : null, body: body ? JSON.stringify(body) : null,

View file

@ -15,7 +15,10 @@ async function getCurrentUser() {
try { try {
return await fetchAPI<MeUser>("/users/@me"); return await fetchAPI<MeUser>("/users/@me");
} catch (e) { } 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"); localStorage.removeItem("pronouns-token");
} }

View file

@ -43,6 +43,7 @@ export enum ErrorCode {
InvalidState = 1001, InvalidState = 1001,
InvalidOAuthCode = 1002, InvalidOAuthCode = 1002,
InvalidToken = 1003,
UserNotFound = 2001, UserNotFound = 2001,
} }

View file

@ -55,7 +55,7 @@ export default function Discord() {
console.log("token:", resp.token); console.log("token:", resp.token);
localStorage.setItem("pronouns-token", resp.token); localStorage.setItem("pronouns-token", resp.token);
if (resp.user) setUser(resp.user as MeUser); setUser(resp.user);
}, },
(err) => { (err) => {
console.log(err); console.log(err);

View file

@ -11,7 +11,6 @@ export default defineConfig({
// assumes port 8080 in .env for development // assumes port 8080 in .env for development
target: "http://localhost:8080", target: "http://localhost:8080",
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
}, },
}, },
}, },