mirror of https://github.com/lus/pasty.git
Compare commits
5 Commits
941da057ae
...
6260f20fc4
Author | SHA1 | Date |
---|---|---|
Lukas Schulte Pelkum | 6260f20fc4 | |
Lukas Schulte Pelkum | dc16506932 | |
Lukas Schulte Pelkum | a24be8b2ff | |
Lukas Schulte Pelkum | 4ce806945d | |
Lukas Schulte Pelkum | a53bd39dbd |
|
@ -1,4 +1,3 @@
|
|||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/jetbrains+all,go
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=jetbrains+all,go
|
||||
|
||||
|
@ -114,6 +113,4 @@ modules.xml
|
|||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/jetbrains+all,go
|
||||
|
||||
web/*.gz
|
||||
data/
|
||||
.env
|
||||
.env
|
||||
|
|
|
@ -22,6 +22,6 @@ RUN go build \
|
|||
FROM gcr.io/distroless/base:latest
|
||||
WORKDIR /root
|
||||
COPY --from=build /app/pasty .
|
||||
COPY web ./web/
|
||||
COPY internal/web/web ./web/
|
||||
EXPOSE 8080
|
||||
CMD ["./pasty"]
|
|
@ -2,12 +2,15 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/lus/pasty/internal/config"
|
||||
"github.com/lus/pasty/internal/meta"
|
||||
"github.com/lus/pasty/internal/storage"
|
||||
"github.com/lus/pasty/internal/storage/postgres"
|
||||
"github.com/lus/pasty/internal/web"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
@ -65,6 +68,36 @@ func main() {
|
|||
}
|
||||
}()
|
||||
|
||||
// Start the web server
|
||||
log.Info().Str("address", cfg.WebAddress).Msg("Starting the web server...")
|
||||
var adminTokens []string
|
||||
if cfg.ModificationTokenMaster != "" {
|
||||
adminTokens = []string{cfg.ModificationTokenMaster}
|
||||
}
|
||||
webServer := &web.Server{
|
||||
Address: cfg.WebAddress,
|
||||
Storage: driver,
|
||||
HastebinSupport: cfg.HastebinSupport,
|
||||
PasteIDLength: cfg.IDLength,
|
||||
PasteIDCharset: cfg.IDCharacters,
|
||||
PasteLengthCap: cfg.LengthCap,
|
||||
ModificationTokensEnabled: cfg.ModificationTokens,
|
||||
ModificationTokenLength: cfg.ModificationTokenLength,
|
||||
ModificationTokenCharset: cfg.ModificationTokenCharacters,
|
||||
AdminTokens: adminTokens,
|
||||
}
|
||||
go func() {
|
||||
if err := webServer.Start(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatal().Err(err).Msg("Could not start the web server.")
|
||||
}
|
||||
}()
|
||||
defer func() {
|
||||
log.Info().Msg("Shutting down the web server...")
|
||||
if err := webServer.Shutdown(context.Background()); err != nil {
|
||||
log.Err(err).Msg("Could not shut down the web server.")
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for an interrupt signal
|
||||
log.Info().Msg("The application has been started. Use Ctrl+C to shut it down.")
|
||||
shutdownChan := make(chan os.Signal, 1)
|
||||
|
|
|
@ -6,6 +6,7 @@ const devEnvironmentName = "dev"
|
|||
|
||||
var (
|
||||
Environment = devEnvironmentName
|
||||
Version = "dev"
|
||||
)
|
||||
|
||||
func IsProdEnvironment() bool {
|
||||
|
|
|
@ -54,7 +54,6 @@ func (driver *Driver) Initialize(ctx context.Context) error {
|
|||
pool.Close()
|
||||
return err
|
||||
}
|
||||
log.Info().Msg("Successfully performed PostgreSQL database migrations.")
|
||||
|
||||
driver.connPool = pool
|
||||
driver.pastes = &pasteRepository{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const API_BASE_URL = location.protocol + "//" + location.host + "/web/v2";
|
||||
const API_BASE_URL = location.protocol + "//" + location.host + "/api/v2";
|
||||
|
||||
export async function getAPIInformation() {
|
||||
return fetch(API_BASE_URL + "/info");
|
|
@ -64,20 +64,20 @@ export async function initialize() {
|
|||
}
|
||||
|
||||
if (location.pathname !== "/") {
|
||||
// Extract the pastes data (ID and language)
|
||||
// Extract the paste data (ID and language)
|
||||
const split = location.pathname.replace("/", "").split(".");
|
||||
const pasteID = split[0];
|
||||
const language = split[1];
|
||||
|
||||
// Try to retrieve the pastes data from the API
|
||||
// Try to retrieve the paste data from the API
|
||||
const response = await API.getPaste(pasteID);
|
||||
if (!response.ok) {
|
||||
Notifications.error("Could not load pastes: <b>" + await response.text() + "</b>");
|
||||
Notifications.error("Could not load paste: <b>" + await response.text() + "</b>");
|
||||
setTimeout(() => location.replace(location.protocol + "//" + location.host), 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the persistent pastes data
|
||||
// Set the persistent paste data
|
||||
PASTE_ID = pasteID;
|
||||
LANGUAGE = language;
|
||||
|
||||
|
@ -95,7 +95,7 @@ export async function initialize() {
|
|||
ENCRYPTION_IV = json.metadata.pf_encryption.iv;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
Notifications.error("Could not decrypt pastes; make sure the decryption key is correct.");
|
||||
Notifications.error("Could not decrypt paste; make sure the decryption key is correct.");
|
||||
setTimeout(() => location.replace(location.protocol + "//" + location.host), 3000);
|
||||
return;
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ export async function initialize() {
|
|||
// Fill the code block with the just received data
|
||||
updateCode();
|
||||
} else {
|
||||
// Give the user the opportunity to pastes his code
|
||||
// Give the user the opportunity to paste his code
|
||||
INPUT_ELEMENT.classList.remove("hidden");
|
||||
INPUT_ELEMENT.focus();
|
||||
LIFETIME_CONTAINER_ELEMENT.classList.remove("hidden");
|
||||
|
@ -138,7 +138,7 @@ async function loadAPIInformation() {
|
|||
// Display the API version
|
||||
document.getElementById("version").innerText = API_INFORMATION.version;
|
||||
|
||||
// Display the pastes lifetime
|
||||
// Display the paste lifetime
|
||||
document.getElementById("lifetime").innerText = Duration.format(API_INFORMATION.pasteLifetime);
|
||||
}
|
||||
|
||||
|
@ -215,7 +215,7 @@ function toggleEditMode() {
|
|||
function setupKeybinds() {
|
||||
window.addEventListener("keydown", (event) => {
|
||||
// All keybinds in the default button set include the CTRL key
|
||||
if ((EDIT_MODE && !event.ctrlKey && event.code !== "Escape") || (!EDIT_MODE && !event.ctrlKey)) {
|
||||
if ((EDIT_MODE && !event.ctrlKey && !event.metaKey && event.code !== "Escape") || (!EDIT_MODE && !event.ctrlKey && !event.metaKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -289,7 +289,7 @@ function setupButtonFunctionality() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Encrypt the pastes if needed
|
||||
// Encrypt the paste if needed
|
||||
let value = INPUT_ELEMENT.value;
|
||||
let metadata;
|
||||
let key;
|
||||
|
@ -305,20 +305,20 @@ function setupButtonFunctionality() {
|
|||
key = encrypted.key;
|
||||
}
|
||||
|
||||
// Try to create the pastes
|
||||
// Try to create the paste
|
||||
const response = await API.createPaste(value, metadata);
|
||||
if (!response.ok) {
|
||||
Notifications.error("Error while creating pastes: <b>" + await response.text() + "</b>");
|
||||
Notifications.error("Error while creating paste: <b>" + await response.text() + "</b>");
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
// Display the modification token if provided
|
||||
if (data.modificationToken) {
|
||||
prompt("The modification token for your pastes is:", data.modificationToken);
|
||||
prompt("The modification token for your paste is:", data.modificationToken);
|
||||
}
|
||||
|
||||
// Redirect the user to his newly created pastes
|
||||
// Redirect the user to his newly created paste
|
||||
location.replace(location.protocol + "//" + location.host + "/" + data.id + (key ? "#" + key : ""));
|
||||
});
|
||||
});
|
||||
|
@ -333,10 +333,10 @@ function setupButtonFunctionality() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Try to delete the pastes
|
||||
// Try to delete the paste
|
||||
const response = await API.deletePaste(PASTE_ID, modificationToken);
|
||||
if (!response.ok) {
|
||||
Notifications.error("Error while deleting pastes: <b>" + await response.text() + "</b>");
|
||||
Notifications.error("Error while deleting paste: <b>" + await response.text() + "</b>");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -369,17 +369,17 @@ function setupButtonFunctionality() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Re-encrypt the pastes data if needed
|
||||
// Re-encrypt the paste data if needed
|
||||
let value = INPUT_ELEMENT.value;
|
||||
if (ENCRYPTION_KEY && ENCRYPTION_IV) {
|
||||
const encrypted = await Encryption.encrypt(await Encryption.encryptionDataFromHex(ENCRYPTION_KEY, ENCRYPTION_IV), value);
|
||||
value = encrypted.result;
|
||||
}
|
||||
|
||||
// Try to edit the pastes
|
||||
// Try to edit the paste
|
||||
const response = await API.editPaste(PASTE_ID, modificationToken, value);
|
||||
if (!response.ok) {
|
||||
Notifications.error("Error while editing pastes: <b>" + await response.text() + "</b>");
|
||||
Notifications.error("Error while editing paste: <b>" + await response.text() + "</b>");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -387,13 +387,13 @@ function setupButtonFunctionality() {
|
|||
CODE = INPUT_ELEMENT.value;
|
||||
updateCode();
|
||||
toggleEditMode();
|
||||
Notifications.success("Successfully edited pastes.");
|
||||
Notifications.success("Successfully edited paste.");
|
||||
});
|
||||
|
||||
BUTTON_TOGGLE_ENCRYPTION_ELEMENT.addEventListener("click", () => {
|
||||
const active = BUTTON_TOGGLE_ENCRYPTION_ELEMENT.classList.toggle("active");
|
||||
localStorage.setItem("encryption", active);
|
||||
Notifications.success((active ? "Enabled" : "Disabled") + " automatic pastes encryption.");
|
||||
Notifications.success((active ? "Enabled" : "Disabled") + " automatic paste encryption.");
|
||||
});
|
||||
|
||||
BUTTON_REPORT_ELEMENT.addEventListener("click", async () => {
|
||||
|
@ -403,17 +403,17 @@ function setupButtonFunctionality() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Try to report the pastes
|
||||
// Try to report the paste
|
||||
const response = await API.reportPaste(PASTE_ID, reason);
|
||||
if (!response.ok) {
|
||||
Notifications.error("Error while reporting pastes: <b>" + await response.text() + "</b>");
|
||||
Notifications.error("Error while reporting paste: <b>" + await response.text() + "</b>");
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the response message
|
||||
const data = await response.json();
|
||||
if (!data.success) {
|
||||
Notifications.error("Error while reporting pastes: <b>" + data.message + "</b>");
|
||||
Notifications.error("Error while reporting paste: <b>" + data.message + "</b>");
|
||||
return;
|
||||
}
|
||||
Notifications.success(data.message);
|
|
@ -0,0 +1,77 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed frontend/*
|
||||
var frontend embed.FS
|
||||
|
||||
func frontendHandler(notFoundHandler http.HandlerFunc) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
path := strings.TrimSpace(strings.TrimSuffix(request.URL.Path, "/"))
|
||||
|
||||
isFirstLevel := strings.Count(path, "/") <= 1
|
||||
|
||||
file, err := frontend.Open(filepath.Join("frontend", path))
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
if isFirstLevel {
|
||||
serveIndexFile(writer, request)
|
||||
} else {
|
||||
notFoundHandler(writer, request)
|
||||
}
|
||||
return
|
||||
}
|
||||
writeErr(writer, err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
}()
|
||||
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
writeErr(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
if fileInfo.IsDir() {
|
||||
if isFirstLevel {
|
||||
serveIndexFile(writer, request)
|
||||
} else {
|
||||
notFoundHandler(writer, request)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
writeErr(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
writer.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(fileInfo.Name())))
|
||||
writer.Header().Set("Content-Length", strconv.Itoa(len(content)))
|
||||
_, _ = writer.Write(content)
|
||||
}
|
||||
}
|
||||
|
||||
func serveIndexFile(writer http.ResponseWriter, _ *http.Request) {
|
||||
indexFile, err := frontend.ReadFile("frontend/index.html")
|
||||
if err != nil {
|
||||
writeErr(writer, err)
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Type", "text/html")
|
||||
writer.Header().Set("Content-Length", strconv.Itoa(len(indexFile)))
|
||||
_, _ = writer.Write(indexFile)
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/lus/pasty/internal/meta"
|
||||
"github.com/lus/pasty/internal/pastes"
|
||||
"github.com/lus/pasty/internal/storage"
|
||||
"net/http"
|
||||
)
|
||||
|
@ -34,16 +37,53 @@ type Server struct {
|
|||
|
||||
// The administration tokens.
|
||||
AdminTokens []string
|
||||
|
||||
httpServer *http.Server
|
||||
}
|
||||
|
||||
func (server *Server) Start() error {
|
||||
router := chi.NewRouter()
|
||||
|
||||
// Register the web frontend handler
|
||||
router.Get("/*", frontendHandler(router.NotFoundHandler()))
|
||||
|
||||
// Register the raw paste handler
|
||||
router.With(server.v2MiddlewareInjectPaste).Get("/{paste_id}/raw", func(writer http.ResponseWriter, request *http.Request) {
|
||||
paste, ok := request.Context().Value("paste").(*pastes.Paste)
|
||||
if !ok {
|
||||
writeString(writer, http.StatusInternalServerError, "missing paste object")
|
||||
return
|
||||
}
|
||||
_, _ = writer.Write([]byte(paste.Content))
|
||||
})
|
||||
|
||||
// Register the paste API endpoints
|
||||
router.Get("/api/*", router.NotFoundHandler())
|
||||
router.With(server.v2MiddlewareInjectPaste).Get("/api/v2/pastes/{paste_id}", server.v2EndpointGetPaste)
|
||||
router.Post("/api/v2/pastes", server.v2EndpointCreatePaste)
|
||||
router.With(server.v2MiddlewareInjectPaste, server.v2MiddlewareAuthorize).Patch("/api/v2/pastes/{paste_id}", server.v2EndpointModifyPaste)
|
||||
router.Delete("/api/v2/pastes/{paste_id}", server.v2EndpointDeletePaste)
|
||||
router.With(server.v2MiddlewareInjectPaste, server.v2MiddlewareAuthorize).Delete("/api/v2/pastes/{paste_id}", server.v2EndpointDeletePaste)
|
||||
router.Get("/api/v2/info", func(writer http.ResponseWriter, request *http.Request) {
|
||||
writeJSONOrErr(writer, http.StatusOK, map[string]any{
|
||||
"version": meta.Version,
|
||||
"modificationTokens": server.ModificationTokensEnabled,
|
||||
"reports": false, // TODO: Return report state
|
||||
"pasteLifetime": -1, // TODO: Return paste lifetime
|
||||
})
|
||||
})
|
||||
|
||||
return http.ListenAndServe(server.Address, router)
|
||||
// Start the HTTP server
|
||||
server.httpServer = &http.Server{
|
||||
Addr: server.Address,
|
||||
Handler: router,
|
||||
}
|
||||
return server.httpServer.ListenAndServe()
|
||||
}
|
||||
|
||||
func (server *Server) Shutdown(ctx context.Context) error {
|
||||
if err := server.httpServer.Shutdown(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
server.httpServer = nil
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue