diff --git a/.gitignore b/.gitignore index 2bd1803..b361548 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Makefile b/Makefile index 610e5f4..f753760 100644 --- a/Makefile +++ b/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: diff --git a/README.md b/README.md index d3705e5..c7ee84c 100644 --- a/README.md +++ b/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 diff --git a/backend/main.go b/backend/main.go index 7d4c64c..16acd2b 100644 --- a/backend/main.go +++ b/backend/main.go @@ -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) +} diff --git a/backend/routes/auth/discord.go b/backend/routes/auth/discord.go index 524914d..af455ba 100644 --- a/backend/routes/auth/discord.go +++ b/backend/routes/auth/discord.go @@ -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 } diff --git a/frontend/data.go b/frontend/data.go new file mode 100644 index 0000000..64d28ba --- /dev/null +++ b/frontend/data.go @@ -0,0 +1,6 @@ +package frontend + +import "embed" + +//go:embed dist/* +var Data embed.FS diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5223bf4..879aab3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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() { } /> - } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/frontend/src/lib/Navigation.tsx b/frontend/src/lib/Navigation.tsx index 812e0ae..7a16ba6 100644 --- a/frontend/src/lib/Navigation.tsx +++ b/frontend/src/lib/Navigation.tsx @@ -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("/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 ? ( <> - @{user.username} + @{user.username} Settings Log out diff --git a/frontend/src/lib/fetch.ts b/frontend/src/lib/fetch.ts index 79015a3..c2cbfc0 100644 --- a/frontend/src/lib/fetch.ts +++ b/frontend/src/lib/fetch.ts @@ -6,10 +6,18 @@ export default async function fetchAPI( 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, diff --git a/frontend/src/lib/store.ts b/frontend/src/lib/store.ts index c041382..8cceccb 100644 --- a/frontend/src/lib/store.ts +++ b/frontend/src/lib/store.ts @@ -15,7 +15,10 @@ async function getCurrentUser() { try { return await fetchAPI("/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"); } diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 7f47cc6..d251838 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -43,6 +43,7 @@ export enum ErrorCode { InvalidState = 1001, InvalidOAuthCode = 1002, + InvalidToken = 1003, UserNotFound = 2001, } diff --git a/frontend/src/pages/login/Discord.tsx b/frontend/src/pages/login/Discord.tsx index 0048a9e..c27ff50 100644 --- a/frontend/src/pages/login/Discord.tsx +++ b/frontend/src/pages/login/Discord.tsx @@ -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); diff --git a/vite.config.ts b/vite.config.ts index 5715bac..cd6dede 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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/, ""), }, }, },