no longer uploading files to scanners, sharing filesystem instead

This commit is contained in:
Johannes Bülow 2025-05-21 08:52:48 +02:00
parent 2638e5f9a3
commit 34ac340cad
Signed by: jmb
GPG key ID: B56971CF7B8F83A6
12 changed files with 45 additions and 149 deletions

View file

@ -8,7 +8,7 @@ deps:
build: build:
echo "Building..." echo "Building..."
tailwindcss -o web/assets/styles.css tailwindcss -o server/web/assets/styles.css
templ generate templ generate
go build -o main.out server/main.go go build -o main.out server/main.go

4
go.mod
View file

@ -4,22 +4,18 @@ go 1.24.1
require ( require (
github.com/a-h/templ v0.3.857 github.com/a-h/templ v0.3.857
github.com/gorilla/handlers v1.5.2
github.com/gorilla/securecookie v1.1.2 github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.4.0 github.com/gorilla/sessions v1.4.0
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.27 github.com/mattn/go-sqlite3 v1.14.27
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.20.1 github.com/spf13/viper v1.20.1
golang.org/x/crypto v0.32.0 golang.org/x/crypto v0.32.0
) )
require ( require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect

10
go.sum
View file

@ -2,12 +2,9 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/a-h/templ v0.3.857 h1:6EqcJuGZW4OL+2iZ3MD+NnIcG7nGkaQeF2Zq5kf9ZGg= github.com/a-h/templ v0.3.857 h1:6EqcJuGZW4OL+2iZ3MD+NnIcG7nGkaQeF2Zq5kf9ZGg=
github.com/a-h/templ v0.3.857/go.mod h1:qhrhAkRFubE7khxLZHsBFHfX+gWwVNKbzKeF9GlPV4M= github.com/a-h/templ v0.3.857/go.mod h1:qhrhAkRFubE7khxLZHsBFHfX+gWwVNKbzKeF9GlPV4M=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
@ -20,14 +17,10 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@ -47,7 +40,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
@ -56,8 +48,6 @@ github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=

View file

@ -2,11 +2,11 @@ import os
class Config: class Config:
# Read values from environment variables or use defaults # Read values from environment variables or use defaults
UPLOAD_FOLDER = os.environ.get("UPLOAD_FOLDER", "/tmp/uploads") FILE_DIRECTORY = os.environ.get("FILE_DIRECTORY", "/tmp/uploads")
HOST = os.environ.get("HOST", "127.0.0.1") HOST = os.environ.get("HOST", "127.0.0.1")
PORT = int(os.environ.get("PORT", 5000)) PORT = int(os.environ.get("PORT", 5000))
DEBUG = os.environ.get("DEBUG", "False").lower() in ("true", "1") DEBUG = os.environ.get("DEBUG", "False").lower() in ("true", "1")
# Ensure upload directory exists # Ensure upload directory exists
if not os.path.exists(Config.UPLOAD_FOLDER): if not os.path.exists(Config.FILE_DIRECTORY):
os.makedirs(Config.UPLOAD_FOLDER) os.makedirs(Config.FILE_DIRECTORY)

View file

@ -1,20 +1,15 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from os import path
from oletools import olevba, mraptor from oletools import olevba, mraptor
from utils.file_handler import allowed_file, save_file, delete_file import config
mraptor_bp = Blueprint('mraptor', __name__) mraptor_bp = Blueprint('mraptor', __name__)
@mraptor_bp.route('/analyze', methods=['POST']) @mraptor_bp.route('/analyze', methods=['POST'])
def analyze_mraptor(): def analyze_mraptor():
if 'file' not in request.files: data = request.form
return jsonify({'error': 'No file uploaded'}), 400 file = data['file']
filepath = path.join(config.Config.FILE_DIRECTORY, file)
file = request.files['file']
if file.filename == '' or not allowed_file(file.filename):
return jsonify({'error': 'Invalid or unsupported file type'}), 400
filepath = save_file(file)
# Analyze with olevba # Analyze with olevba
vbaparser = olevba.VBA_Parser(filepath) vbaparser = olevba.VBA_Parser(filepath)
if vbaparser.detect_vba_macros(): if vbaparser.detect_vba_macros():
@ -22,15 +17,12 @@ def analyze_mraptor():
try: try:
vba_code = vbaparser.get_vba_code_all_modules() vba_code = vbaparser.get_vba_code_all_modules()
except Exception as e: except Exception as e:
delete_file(filepath)
return jsonify({'error': e}) return jsonify({'error': e})
delete_file(filepath)
raptor = mraptor.MacroRaptor(vba_code) raptor = mraptor.MacroRaptor(vba_code)
raptor.scan() raptor.scan()
if raptor.suspicious: if raptor.suspicious:
return jsonify({'filename': file.filename, 'result': mraptor.Result_Suspicious, 'flags': raptor.get_flags(), 'matches': raptor.matches}) return jsonify({'filename': file, 'result': mraptor.Result_Suspicious, 'flags': raptor.get_flags(), 'matches': raptor.matches})
else: else:
return jsonify({'filename': file.filename, 'result': mraptor.Result_MacroOK, 'flags': raptor.get_flags(), 'matches': raptor.matches}) return jsonify({'filename': file, 'result': mraptor.Result_MacroOK, 'flags': raptor.get_flags(), 'matches': raptor.matches})
else: else:
delete_file(filepath) return jsonify({'filename': file, 'result': mraptor.Result_NoMacro})
return jsonify({'filename': file.filename, 'result': mraptor.Result_NoMacro})

View file

@ -1,24 +1,18 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from os import path
import oletools.oleid import oletools.oleid
from utils.file_handler import allowed_file, delete_file, save_file import config
oleid_bp = Blueprint('oleid', __name__) oleid_bp = Blueprint('oleid', __name__)
@oleid_bp.route('/analyze', methods=['POST']) @oleid_bp.route('/analyze', methods=['POST'])
def analyze_ole(): def analyze_ole():
if 'file' not in request.files: data = request.form
return jsonify({'error': 'No file uploaded'}), 400 file = data['file']
filepath = path.join(config.Config.FILE_DIRECTORY, file)
file = request.files['file']
if file.filename == '' or not allowed_file(file.filename):
return jsonify({'error': 'Invalid or unsupported file type'}), 400
filepath = save_file(file)
# Analyze with oleid # Analyze with oleid
oid = oletools.oleid.OleID(filepath) oid = oletools.oleid.OleID(filepath)
indicators = oid.check() indicators = oid.check()
results = {indicator.name: indicator.value for indicator in indicators} results = {indicator.name: indicator.value for indicator in indicators}
delete_file(filepath)
return jsonify({'filename': file.filename, 'analysis': results}) return jsonify({'filename': file, 'analysis': results})

View file

@ -1,24 +1,19 @@
from os import path
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
import config
import oletools.olevba import oletools.olevba
from utils.file_handler import allowed_file, save_file, delete_file
olevba_bp = Blueprint('olevba', __name__) olevba_bp = Blueprint('olevba', __name__)
@olevba_bp.route('/analyze', methods=['POST']) @olevba_bp.route('/analyze', methods=['POST'])
def analyze_vba(): def analyze_vba():
if 'file' not in request.files: data = request.form
return jsonify({'error': 'No file uploaded'}), 400 file = data['file']
filepath = path.join(config.Config.FILE_DIRECTORY, file)
file = request.files['file']
if file.filename == '' or not allowed_file(file.filename):
return jsonify({'error': 'Invalid or unsupported file type'}), 400
filepath = save_file(file)
# Analyze with olevba # Analyze with olevba
vbaparser = oletools.olevba.VBA_Parser(filepath) vbaparser = oletools.olevba.VBA_Parser(filepath)
results = vbaparser.analyze_macros() results = vbaparser.analyze_macros()
delete_file(filepath)
return jsonify({'filename': file.filename, 'macros': results}) return jsonify({'filename': file, 'macros': results})

View file

@ -1,89 +0,0 @@
/*
Copyright © 2025 Johannes Bülow <johannes.buelow@jmbit.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "scanfile",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.scanfile.yaml)")
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := os.UserHomeDir()
cobra.CheckErr(err)
// Search config in home directory with name ".scanfile" (without extension).
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".scanfile")
}
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}

View file

@ -21,13 +21,19 @@ import (
"log/slog" "log/slog"
"os" "os"
"git.jmbit.de/jmb/scanfile/server/cmd" "git.jmbit.de/jmb/scanfile/server/internal/config"
"git.jmbit.de/jmb/scanfile/server/internal/server"
"github.com/spf13/viper"
) )
func main() { func main() {
log.SetOutput(os.Stderr) log.SetOutput(os.Stderr)
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil))) slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, nil)))
//TODO don't use cobra for this config.ReadConfigFile("")
cmd.Execute() if viper.GetBool("web.tls") {
server.NewServer().ListenAndServeTLS(viper.GetString("web.cert"), viper.GetString("web.key"))
} else {
server.NewServer().ListenAndServe()
}
} }

1
server/web/assets/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -27,6 +27,7 @@ templ Base(title string) {
<p>{viper.GetString("ui.name")} by <a href={templ.URL(viper.GetString("ui.byurl"))}>{viper.GetString("ui.byname")}</a> licenced under AGPLv3, </p> <p>{viper.GetString("ui.name")} by <a href={templ.URL(viper.GetString("ui.byurl"))}>{viper.GetString("ui.byname")}</a> licenced under AGPLv3, </p>
<p><a href={templ.URL(viper.GetString("ui.source"))}>Source</a></p> <p><a href={templ.URL(viper.GetString("ui.source"))}>Source</a></p>
</div> </div>
<script src="/assets/htmx.min.js"></script>
</footer> </footer>
</body> </body>
</html> </html>

10
tailwind.config.js Normal file
View file

@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./server/web/*.{go,js,templ,html}"
],
theme: {
extend: {},
},
plugins: [],
}