forked from mirrors/pronouns.cc
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*
|
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
|
||||||
|
|
6
Makefile
6
Makefile
|
@ -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:
|
||||||
|
|
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
|
- 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>
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
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 "./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 />} />
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ export enum ErrorCode {
|
||||||
|
|
||||||
InvalidState = 1001,
|
InvalidState = 1001,
|
||||||
InvalidOAuthCode = 1002,
|
InvalidOAuthCode = 1002,
|
||||||
|
InvalidToken = 1003,
|
||||||
|
|
||||||
UserNotFound = 2001,
|
UserNotFound = 2001,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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/, ""),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue