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*
node_modules
dist
frontend/dist/*
dist-ssr
!frontend/dist/.empty
*.local
# Editor directories and files

View file

@ -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:

View file

@ -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>

View file

@ -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)
}

View file

@ -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
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 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 />} />

View file

@ -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>
</>

View file

@ -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,

View file

@ -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");
}

View file

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

View file

@ -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);

View file

@ -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/, ""),
},
},
},