mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-06 07:54:22 +00:00
3
.gitignore
vendored
3
.gitignore
vendored
@@ -129,6 +129,9 @@ dist
|
|||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
.vite
|
||||||
|
|
||||||
|
|
||||||
##### Go #####
|
##### Go #####
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
@@ -32,20 +30,16 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Workdir
|
// Initialize filesystem
|
||||||
workdir := os.Getenv("NOVAMD_WORKDIR")
|
workdir := os.Getenv("NOVAMD_WORKDIR")
|
||||||
if workdir == "" {
|
if workdir == "" {
|
||||||
workdir = "./data"
|
workdir = "./data"
|
||||||
}
|
}
|
||||||
|
|
||||||
fs := filesystem.New(workdir)
|
fs := filesystem.New(workdir)
|
||||||
|
|
||||||
// User service
|
// Initialize user service
|
||||||
userService := user.NewUserService(database, fs)
|
userService := user.NewUserService(database, fs)
|
||||||
|
if _, err := userService.SetupAdminUser(); err != nil {
|
||||||
// Admin user
|
|
||||||
_, err = userService.SetupAdminUser()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,44 +48,26 @@ func main() {
|
|||||||
r.Use(middleware.Logger)
|
r.Use(middleware.Logger)
|
||||||
r.Use(middleware.Recoverer)
|
r.Use(middleware.Recoverer)
|
||||||
|
|
||||||
// Set up API routes
|
// API routes
|
||||||
r.Route("/api/v1", func(r chi.Router) {
|
r.Route("/api/v1", func(r chi.Router) {
|
||||||
api.SetupRoutes(r, database, fs)
|
api.SetupRoutes(r, database, fs)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set up static file server with path validation
|
// Static file serving
|
||||||
staticPath := os.Getenv("NOVAMD_STATIC_PATH")
|
staticPath := os.Getenv("NOVAMD_STATIC_PATH")
|
||||||
if staticPath == "" {
|
if staticPath == "" {
|
||||||
staticPath = "../frontend/dist"
|
staticPath = "../frontend/dist"
|
||||||
}
|
}
|
||||||
fileServer := http.FileServer(http.Dir(staticPath))
|
|
||||||
r.Get(
|
|
||||||
"/*",
|
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
requestedPath := r.URL.Path
|
|
||||||
|
|
||||||
fullPath := filepath.Join(staticPath, requestedPath)
|
// Handle all other routes with static file server
|
||||||
cleanPath := filepath.Clean(fullPath)
|
r.Get("/*", api.NewStaticHandler(staticPath).ServeHTTP)
|
||||||
|
|
||||||
if !strings.HasPrefix(cleanPath, staticPath) {
|
|
||||||
http.Error(w, "Invalid path", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = os.Stat(cleanPath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
http.ServeFile(w, r, filepath.Join(staticPath, "index.html"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.StripPrefix("/", fileServer).ServeHTTP(w, r)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
port := os.Getenv("NOVAMD_PORT")
|
port := os.Getenv("NOVAMD_PORT")
|
||||||
if port == "" {
|
if port == "" {
|
||||||
port = "8080"
|
port = "8080"
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Server starting on port %s", port)
|
log.Printf("Server starting on port %s", port)
|
||||||
log.Fatal(http.ListenAndServe(":"+port, r))
|
log.Fatal(http.ListenAndServe(":"+port, r))
|
||||||
}
|
}
|
||||||
|
|||||||
70
backend/internal/api/static_handler.go
Normal file
70
backend/internal/api/static_handler.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StaticHandler serves static files with support for SPA routing and pre-compressed files
|
||||||
|
type StaticHandler struct {
|
||||||
|
staticPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStaticHandler(staticPath string) *StaticHandler {
|
||||||
|
return &StaticHandler{
|
||||||
|
staticPath: staticPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *StaticHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get the requested path
|
||||||
|
requestedPath := r.URL.Path
|
||||||
|
fullPath := filepath.Join(h.staticPath, requestedPath)
|
||||||
|
cleanPath := filepath.Clean(fullPath)
|
||||||
|
|
||||||
|
// Security check to prevent directory traversal
|
||||||
|
if !strings.HasPrefix(cleanPath, h.staticPath) {
|
||||||
|
http.Error(w, "Invalid path", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cache headers for assets
|
||||||
|
if strings.HasPrefix(requestedPath, "/assets/") {
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=31536000") // 1 year
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file exists (not counting .gz files)
|
||||||
|
stat, err := os.Stat(cleanPath)
|
||||||
|
if err != nil || stat.IsDir() {
|
||||||
|
// Serve index.html for SPA routing
|
||||||
|
indexPath := filepath.Join(h.staticPath, "index.html")
|
||||||
|
http.ServeFile(w, r, indexPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for pre-compressed version
|
||||||
|
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
gzPath := cleanPath + ".gz"
|
||||||
|
if _, err := os.Stat(gzPath); err == nil {
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
|
||||||
|
// Set proper content type based on original file
|
||||||
|
switch filepath.Ext(cleanPath) {
|
||||||
|
case ".js":
|
||||||
|
w.Header().Set("Content-Type", "application/javascript")
|
||||||
|
case ".css":
|
||||||
|
w.Header().Set("Content-Type", "text/css")
|
||||||
|
case ".html":
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
}
|
||||||
|
|
||||||
|
http.ServeFile(w, r, gzPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve original file
|
||||||
|
http.ServeFile(w, r, cleanPath)
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
"@babel/preset-env",
|
|
||||||
["@babel/preset-react", { "runtime": "automatic" }]
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"@babel/plugin-transform-class-properties",
|
|
||||||
"@babel/plugin-transform-runtime"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
7323
frontend/package-lock.json
generated
7323
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,10 +2,11 @@
|
|||||||
"name": "novamd-frontend",
|
"name": "novamd-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Yet another markdown editor",
|
"description": "Yet another markdown editor",
|
||||||
"main": "index.js",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack serve --mode development --open",
|
"start": "vite",
|
||||||
"build": "webpack --mode production"
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -43,11 +44,16 @@
|
|||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-math": "^6.0.0"
|
"remark-math": "^6.0.0"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"devDependencies": {
|
||||||
"extends": [
|
"@types/react": "^18.2.67",
|
||||||
"react-app",
|
"@types/react-dom": "^18.2.22",
|
||||||
"react-app/jest"
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
]
|
"postcss": "^8.4.47",
|
||||||
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
|
"postcss-simple-vars": "^7.0.1",
|
||||||
|
"sass": "^1.80.4",
|
||||||
|
"vite": "^5.4.10",
|
||||||
|
"vite-plugin-compression2": "^1.3.0"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
@@ -60,26 +66,5 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.25.7",
|
|
||||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
|
||||||
"@babel/plugin-transform-class-properties": "^7.25.7",
|
|
||||||
"@babel/plugin-transform-runtime": "^7.25.7",
|
|
||||||
"@babel/preset-env": "^7.25.7",
|
|
||||||
"@babel/preset-react": "^7.25.7",
|
|
||||||
"babel-loader": "^9.2.1",
|
|
||||||
"css-loader": "^7.1.2",
|
|
||||||
"html-webpack-plugin": "^5.6.0",
|
|
||||||
"postcss": "^8.4.47",
|
|
||||||
"postcss-loader": "^8.1.1",
|
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
|
||||||
"postcss-simple-vars": "^7.0.1",
|
|
||||||
"sass": "^1.79.4",
|
|
||||||
"sass-loader": "^16.0.2",
|
|
||||||
"style-loader": "^4.0.0",
|
|
||||||
"webpack": "^5.94.0",
|
|
||||||
"webpack-cli": "^5.1.4",
|
|
||||||
"webpack-dev-server": "^5.1.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
'postcss-preset-mantine': {},
|
|
||||||
'postcss-simple-vars': {
|
|
||||||
variables: {
|
|
||||||
'mantine-breakpoint-xs': '36em',
|
|
||||||
'mantine-breakpoint-sm': '48em',
|
|
||||||
'mantine-breakpoint-md': '62em',
|
|
||||||
'mantine-breakpoint-lg': '75em',
|
|
||||||
'mantine-breakpoint-xl': '88em',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>NovaMD</title>
|
<title>NovaMD</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./index.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
136
frontend/vite.config.js
Normal file
136
frontend/vite.config.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
import postcssPresetMantine from 'postcss-preset-mantine';
|
||||||
|
import postcssSimpleVars from 'postcss-simple-vars';
|
||||||
|
import { compression } from 'vite-plugin-compression2';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig(({ mode }) => ({
|
||||||
|
plugins: [
|
||||||
|
react({
|
||||||
|
include: ['**/*.jsx', '**/*.js'],
|
||||||
|
}),
|
||||||
|
compression(),
|
||||||
|
],
|
||||||
|
|
||||||
|
root: 'src',
|
||||||
|
publicDir: '../public',
|
||||||
|
|
||||||
|
build: {
|
||||||
|
outDir: '../dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
assetsDir: 'assets',
|
||||||
|
sourcemap: mode === 'development',
|
||||||
|
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
main: path.resolve(__dirname, 'src/index.html'),
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
// React core libraries
|
||||||
|
'react-core': ['react', 'react-dom'],
|
||||||
|
|
||||||
|
// Mantine UI components and related
|
||||||
|
mantine: [
|
||||||
|
'@mantine/core',
|
||||||
|
'@mantine/hooks',
|
||||||
|
'@mantine/modals',
|
||||||
|
'@mantine/notifications',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Editor related packages
|
||||||
|
editor: [
|
||||||
|
'codemirror',
|
||||||
|
'@codemirror/commands',
|
||||||
|
'@codemirror/lang-markdown',
|
||||||
|
'@codemirror/state',
|
||||||
|
'@codemirror/theme-one-dark',
|
||||||
|
'@codemirror/view',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Markdown processing
|
||||||
|
markdown: [
|
||||||
|
'react-markdown',
|
||||||
|
'react-syntax-highlighter',
|
||||||
|
'rehype-katex',
|
||||||
|
'remark-math',
|
||||||
|
'katex',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Icons and utilities
|
||||||
|
utils: [
|
||||||
|
'@tabler/icons-react',
|
||||||
|
'@react-hook/resize-observer',
|
||||||
|
'react-arborist',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Optimize chunk naming for better caching
|
||||||
|
chunkFileNames: (chunkInfo) => {
|
||||||
|
const name = chunkInfo.name;
|
||||||
|
if (name === 'react-core') return 'assets/react.[hash].js';
|
||||||
|
if (name === 'mantine') return 'assets/mantine.[hash].js';
|
||||||
|
if (name === 'editor') return 'assets/editor.[hash].js';
|
||||||
|
if (name === 'markdown') return 'assets/markdown.[hash].js';
|
||||||
|
if (name === 'utils') return 'assets/utils.[hash].js';
|
||||||
|
return 'assets/[name].[hash].js';
|
||||||
|
},
|
||||||
|
// Optimize asset naming
|
||||||
|
assetFileNames: 'assets/[name].[hash][extname]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
open: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
define: {
|
||||||
|
'window.API_BASE_URL': JSON.stringify(
|
||||||
|
mode === 'production' ? '/api/v1' : 'http://localhost:8080/api/v1'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
api: 'modern',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
postcss: {
|
||||||
|
plugins: [
|
||||||
|
postcssPresetMantine(),
|
||||||
|
postcssSimpleVars({
|
||||||
|
variables: {
|
||||||
|
'mantine-breakpoint-xs': '36em',
|
||||||
|
'mantine-breakpoint-sm': '48em',
|
||||||
|
'mantine-breakpoint-md': '62em',
|
||||||
|
'mantine-breakpoint-lg': '75em',
|
||||||
|
'mantine-breakpoint-xl': '88em',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
extensions: ['.js', '.jsx', '.json'],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add performance optimization options
|
||||||
|
optimizeDeps: {
|
||||||
|
include: [
|
||||||
|
'react',
|
||||||
|
'react-dom',
|
||||||
|
'@mantine/core',
|
||||||
|
'@mantine/hooks',
|
||||||
|
'codemirror',
|
||||||
|
'react-markdown',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
const webpack = require('webpack');
|
|
||||||
|
|
||||||
module.exports = (env, argv) => {
|
|
||||||
const isProduction = argv.mode === 'production';
|
|
||||||
|
|
||||||
return {
|
|
||||||
entry: './src/index.js',
|
|
||||||
output: {
|
|
||||||
path: path.resolve(__dirname, 'dist'),
|
|
||||||
filename: 'bundle.js',
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.(js|jsx)$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: ['babel-loader'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.scss$/,
|
|
||||||
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
use: ['style-loader', 'css-loader', 'postcss-loader'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: './public/index.html',
|
|
||||||
}),
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'window.API_BASE_URL': JSON.stringify(
|
|
||||||
isProduction ? '/api/v1' : 'http://localhost:8080/api/v1'
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
devServer: {
|
|
||||||
static: {
|
|
||||||
directory: path.join(__dirname, 'public'),
|
|
||||||
},
|
|
||||||
port: 3000,
|
|
||||||
open: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user