initial commit
This commit is contained in:
commit
c21f569144
37 changed files with 3956 additions and 0 deletions
32
.air.toml
Normal file
32
.air.toml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
root = "."
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
bin = "./tmp/main"
|
||||
cmd = "templ generate && tailwindcss -i views/static/css/input.css -o views/static/css/output.css && go build -v -o ./tmp/main ./server/server.go"
|
||||
delay = 500
|
||||
exclude_dir = ["assets", "tmp", "vendor", "data"]
|
||||
exclude_file = []
|
||||
exclude_regex = [".*_templ.go", "output.css"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "templ", "html", "css"]
|
||||
# kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
send_interrupt = false
|
||||
stop_on_error = true
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.env
|
||||
views/static/css/output.css
|
||||
*_templ.go
|
||||
tmp/*
|
||||
routes/tmp/*
|
||||
views/static/box/*
|
||||
31
Dockerfile
Normal file
31
Dockerfile
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
FROM golang:1.23
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY views views
|
||||
COPY server server
|
||||
COPY database database
|
||||
COPY routes routes
|
||||
|
||||
COPY tailwind.config.js ./
|
||||
COPY go.mod go.sum ./
|
||||
COPY Makefile ./
|
||||
|
||||
# não é legal
|
||||
COPY .env ./
|
||||
|
||||
RUN wget https://github.com/tailwindlabs/tailwindcss/releases/download/v4.0.3/tailwindcss-linux-x64 \
|
||||
--quiet \
|
||||
-O tailwindcss
|
||||
RUN chmod a+x tailwindcss
|
||||
|
||||
RUN go install github.com/a-h/templ/cmd/templ@latest
|
||||
|
||||
RUN templ generate .
|
||||
|
||||
# make não consegue encontrar o tailwind sem que ajustemos o PATH
|
||||
RUN PATH=$PATH:/app make build
|
||||
|
||||
EXPOSE 8888
|
||||
|
||||
CMD ["/app/base"]
|
||||
26
Makefile
Normal file
26
Makefile
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
.PHONY: dev build build-docker tw templ db_teste goose
|
||||
|
||||
include .env
|
||||
|
||||
dev:
|
||||
sudo docker compose up db adminer prometheus -d
|
||||
air
|
||||
|
||||
build: tw templ
|
||||
go build -v -o base ./server
|
||||
|
||||
build-docker:
|
||||
sudo docker compose up --force-recreate --build -d
|
||||
|
||||
tw:
|
||||
tailwindcss -i views/static/css/input.css -o views/static/css/output.css
|
||||
|
||||
templ:
|
||||
templ generate
|
||||
|
||||
|
||||
db-teste: goose
|
||||
psql -h localhost -U ${PG_USER} ${PG_DB} < database/testdata/teste.sql
|
||||
|
||||
goose:
|
||||
goose postgres "postgres://${PG_USER}:${PG_PASS}@${PG_ADDR}/${PG_DB}" up -dir database/migrations
|
||||
6
README.md
Normal file
6
README.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
# **This box URL belongs to me!**
|
||||
|
||||
teste
|
||||
|
||||
<script>alert("if this is executed it means i screwed up")</script>
|
||||
75
database/box.go
Normal file
75
database/box.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"foobar/model"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func SelectBox(boxURL model.BoxURL) (model.Box, error) {
|
||||
row := instance.QueryRow(
|
||||
`SELECT id, edit_code, header, private, moderation, created_at, last_updated_at
|
||||
FROM box WHERE url = $1;`,
|
||||
boxURL,
|
||||
)
|
||||
if row.Err() != nil {
|
||||
return model.Box{}, row.Err()
|
||||
}
|
||||
var (
|
||||
id uuid.UUID
|
||||
edit_code string
|
||||
header *model.Markdown
|
||||
private bool
|
||||
moderation bool
|
||||
createdAt time.Time
|
||||
lastUpdatedAt time.Time
|
||||
)
|
||||
err := row.Scan(&id, &edit_code, &header, &private, &moderation, &createdAt, &lastUpdatedAt)
|
||||
if err != nil {
|
||||
return model.Box{}, err
|
||||
}
|
||||
if header == nil {
|
||||
header = new(model.Markdown)
|
||||
*header = ""
|
||||
}
|
||||
return model.NewBox(id, boxURL, edit_code, *header, private, moderation, createdAt, lastUpdatedAt), nil
|
||||
}
|
||||
|
||||
func UpdateHeader(tx *sql.Tx, boxId uuid.UUID, text model.Markdown) error {
|
||||
result, err := tx.Exec(`UPDATE box SET header = $1 where id = $2`, text, boxId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != 1 {
|
||||
return errNoRowsAffected
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func InsertBox(tx *sql.Tx, box model.Box, editCode string, header model.Markdown) error {
|
||||
result, err := tx.Exec(
|
||||
`INSERT INTO box (id, url, edit_code, header, private, moderation, created_at, last_updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||||
box.ID(), box.Url(), editCode, header, box.Private(), box.Moderation(),
|
||||
box.CreatedAt(), box.LastUpdatedAt(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != 1 {
|
||||
return errNoRowsAffected
|
||||
}
|
||||
return nil
|
||||
}
|
||||
86
database/database.go
Normal file
86
database/database.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
instance *sql.DB
|
||||
|
||||
errNoRowsAffected = fmt.Errorf("no rows affected")
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
embedMigrations embed.FS
|
||||
)
|
||||
|
||||
func New() *sql.DB {
|
||||
|
||||
// Evita criar mais de um banco
|
||||
if instance != nil {
|
||||
return instance
|
||||
}
|
||||
|
||||
// Carrega as variáveis de ambiente definadas no arquivo .env na raiz do projeto.
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Para rodar os testes desse pacote, É necessário que haja um symlink de um .env nesta
|
||||
// pasta (database/) apontando para o .env no topo do projeto, já que 'go test' usa o
|
||||
// diretório raiz do pacote (./database) para executar os testes enquanto 'go run' usa o diretório
|
||||
// raiz do projeto.
|
||||
|
||||
// variáveis ambientais de .env são carregadas para o ambiente do nosso processo, basta
|
||||
// usar a forma padrão para pegar os valores dessas variáveis.
|
||||
var (
|
||||
user = os.Getenv("PG_USER")
|
||||
pass = os.Getenv("PG_PASS")
|
||||
dbName = os.Getenv("PG_DB")
|
||||
hostAddr = os.Getenv("PG_ADDR")
|
||||
)
|
||||
|
||||
connectionString := fmt.Sprintf("postgresql://%s:%s@%s:5432/%s", user, pass, hostAddr, dbName)
|
||||
db, err := sql.Open("pgx", connectionString)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Verifica se a conexão foi um sucesso
|
||||
if err := db.Ping(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
goose.SetBaseFS(embedMigrations)
|
||||
if err := goose.SetDialect("postgres"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Aplica todas as migrações disponíveis
|
||||
if err := goose.Up(db, "migrations"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
instance = db
|
||||
log.Println("Nova instância do banco de dados foi criada.")
|
||||
return instance
|
||||
}
|
||||
|
||||
func MustBeginTx() *sql.Tx {
|
||||
if instance == nil {
|
||||
panic(fmt.Errorf("Banco de dados não foi inicializado. Não foi possível criar nova transação."))
|
||||
}
|
||||
tx, err := instance.BeginTx(context.Background(), nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
82
database/database_test.go
Normal file
82
database/database_test.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"foobar/model"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
_ = New()
|
||||
}
|
||||
|
||||
func TestGetBox(t *testing.T) {
|
||||
_ = New()
|
||||
|
||||
boxURL, err := model.CheckBoxURL("1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b, err := SelectBox(boxURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if b.ID().String() == "" {
|
||||
t.Fatalf("")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertBox(t *testing.T) {
|
||||
|
||||
_ = New()
|
||||
tx := MustBeginTx()
|
||||
defer tx.Rollback()
|
||||
|
||||
url, err := model.CheckBoxURL("foobar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
box := model.NewBox(uuid.New(), url, "foobar", "", false, false, time.Now(), time.Now())
|
||||
|
||||
err = InsertBox(tx, box, "foobar", "foobar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterFile(t *testing.T) {
|
||||
_ = New()
|
||||
tx := MustBeginTx()
|
||||
defer tx.Rollback()
|
||||
|
||||
url, err := model.CheckBoxURL("foobar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
box := model.NewBox(uuid.New(), url, "foobar", "", false, false, time.Now(), time.Now())
|
||||
|
||||
err = InsertBox(tx, box, "foobar", "foobar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var file = model.NewFile(
|
||||
uuid.New(), "foobar", 6, time.Now(), model.FileMime("plain/text"), md5.Sum([]byte("foobar")),
|
||||
)
|
||||
|
||||
err = InsertFile(tx, box.ID(), file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
65
database/file.go
Normal file
65
database/file.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"foobar/model"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func InsertFile(tx *sql.Tx, boxId uuid.UUID, file model.File) error {
|
||||
|
||||
row, err := tx.Exec(
|
||||
`INSERT INTO file (id, id_box, name, created_at, size, mime, md5)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7);`,
|
||||
file.ID(), boxId, file.Name(), file.CreatedAt(),
|
||||
file.Size(), file.Mime(), file.Checksum(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
affected, err := row.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected != 1 {
|
||||
return errNoRowsAffected
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SelectBoxFiles(boxID uuid.UUID) (f []model.File, e error) {
|
||||
// TODO: teste
|
||||
row, err := instance.Query(
|
||||
`SELECT id, name, size, created_at, mime, md5
|
||||
FROM file WHERE id_box = $1 ORDER BY created_at DESC`,
|
||||
boxID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for row.Next() {
|
||||
var (
|
||||
id uuid.UUID
|
||||
name model.Filename
|
||||
size int64
|
||||
createdAt time.Time
|
||||
mime model.FileMime
|
||||
encodedMd5 string
|
||||
)
|
||||
err = row.Scan(&id, &name, &size, &createdAt, &mime, &encodedMd5)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decodedMD5, err := hex.DecodeString(encodedMd5)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f = append(f, model.NewFile(id, name, size, createdAt, mime, model.MD5Checksum(decodedMD5)))
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
36
database/migrations/20250202024620_init_usuario.sql
Normal file
36
database/migrations/20250202024620_init_usuario.sql
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- Diff code generated with pgModeler (PostgreSQL Database Modeler)
|
||||
-- pgModeler version: 1.2.0-alpha1
|
||||
-- Diff date: 2025-02-01 23:56:43
|
||||
-- Source model: dev1
|
||||
-- Database: dev1
|
||||
-- PostgreSQL version: 16.0
|
||||
|
||||
-- [ Diff summary ]
|
||||
-- Dropped objects: 0
|
||||
-- Created objects: 1
|
||||
-- Changed objects: 0
|
||||
|
||||
SET search_path=public,pg_catalog;
|
||||
-- ddl-end --
|
||||
|
||||
|
||||
-- [ Created objects ] --
|
||||
-- object: public.usuario | type: TABLE --
|
||||
-- DROP TABLE IF EXISTS public.usuario CASCADE;
|
||||
CREATE TABLE public.usuario (
|
||||
id integer NOT NULL GENERATED ALWAYS AS IDENTITY ,
|
||||
email text NOT NULL,
|
||||
senha_hash text NOT NULL,
|
||||
nome text NOT NULL,
|
||||
ctime timestamp NOT NULL DEFAULT current_timestamp,
|
||||
CONSTRAINT usuario_pk PRIMARY KEY (id)
|
||||
);
|
||||
-- ddl-end --
|
||||
ALTER TABLE public.usuario OWNER TO dev;
|
||||
-- ddl-end --
|
||||
|
||||
|
||||
36
database/migrations/20250412193502_file.sql
Normal file
36
database/migrations/20250412193502_file.sql
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
-- +goose Up
|
||||
-- Diff code generated with pgModeler (PostgreSQL Database Modeler)
|
||||
-- pgModeler version: 1.2.0-alpha1
|
||||
-- Diff date: 2025-04-12 16:39:19
|
||||
-- Source model: dev1
|
||||
-- Database: dev1
|
||||
-- PostgreSQL version: 17.0
|
||||
|
||||
-- [ Diff summary ]
|
||||
-- Dropped objects: 1
|
||||
-- Created objects: 1
|
||||
-- Changed objects: 0
|
||||
|
||||
SET search_path=public,pg_catalog;
|
||||
-- ddl-end --
|
||||
|
||||
|
||||
-- [ Dropped objects ] --
|
||||
DROP TABLE IF EXISTS public.usuario CASCADE;
|
||||
-- ddl-end --
|
||||
|
||||
|
||||
-- [ Created objects ] --
|
||||
-- object: public.file | type: TABLE --
|
||||
-- DROP TABLE IF EXISTS public.file CASCADE;
|
||||
CREATE TABLE public.file (
|
||||
id uuid NOT NULL,
|
||||
name varchar(256) NOT NULL,
|
||||
size integer NOT NULL,
|
||||
created_at timestamp NOT NULL,
|
||||
CONSTRAINT file_pk PRIMARY KEY (id)
|
||||
);
|
||||
-- ddl-end --
|
||||
ALTER TABLE public.file OWNER TO dev;
|
||||
-- ddl-end --
|
||||
|
||||
32
database/migrations/20250414041807_file_mime.sql
Normal file
32
database/migrations/20250414041807_file_mime.sql
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
-- +goose Up
|
||||
-- Diff code generated with pgModeler (PostgreSQL Database Modeler)
|
||||
-- pgModeler version: 1.2.0-alpha1
|
||||
-- Diff date: 2025-04-14 01:18:24
|
||||
-- Source model: dev1
|
||||
-- Database: dev1
|
||||
-- PostgreSQL version: 17.0
|
||||
|
||||
-- [ Diff summary ]
|
||||
-- Dropped objects: 0
|
||||
-- Created objects: 2
|
||||
-- Changed objects: 0
|
||||
|
||||
SET search_path=public,pg_catalog;
|
||||
-- ddl-end --
|
||||
|
||||
|
||||
-- [ Created objects ] --
|
||||
-- object: mime | type: COLUMN --
|
||||
ALTER TABLE public.file ADD COLUMN mime text;
|
||||
-- ddl-end --
|
||||
|
||||
UPDATE public.file SET mime = '';
|
||||
|
||||
ALTER TABLE public.file ALTER COLUMN mime SET NOT NULL;
|
||||
|
||||
|
||||
-- object: message | type: COLUMN --
|
||||
ALTER TABLE public.file ADD COLUMN message varchar(2048);
|
||||
-- ddl-end --
|
||||
|
||||
|
||||
71
database/migrations/20250415031815_box_table.sql
Normal file
71
database/migrations/20250415031815_box_table.sql
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
-- +goose Up
|
||||
-- Diff code generated with pgModeler (PostgreSQL Database Modeler)
|
||||
-- pgModeler version: 1.2.0-alpha1
|
||||
-- Diff date: 2025-04-15 00:18:26
|
||||
-- Source model: dev1
|
||||
-- Database: dev1
|
||||
-- PostgreSQL version: 17.0
|
||||
|
||||
-- [ Diff summary ]
|
||||
-- Dropped objects: 0
|
||||
-- Created objects: 6
|
||||
-- Changed objects: 0
|
||||
|
||||
SET search_path=public,pg_catalog;
|
||||
-- ddl-end --
|
||||
|
||||
|
||||
-- [ Created objects ] --
|
||||
-- object: width | type: COLUMN --
|
||||
ALTER TABLE public.file ADD COLUMN width smallint;
|
||||
-- ddl-end --
|
||||
|
||||
|
||||
-- object: height | type: COLUMN --
|
||||
ALTER TABLE public.file ADD COLUMN height smallint;
|
||||
-- ddl-end --
|
||||
|
||||
|
||||
-- object: public.box | type: TABLE --
|
||||
-- DROP TABLE IF EXISTS public.box CASCADE;
|
||||
CREATE TABLE public.box (
|
||||
id uuid NOT NULL,
|
||||
url varchar(256) NOT NULL,
|
||||
edit_code varchar(64) NOT NULL,
|
||||
header varchar(4096),
|
||||
private boolean NOT NULL DEFAULT false,
|
||||
moderation boolean NOT NULL DEFAULT false,
|
||||
created_at timestamp NOT NULL DEFAULT current_timestamp,
|
||||
last_updated_at timestamp NOT NULL DEFAULT current_timestamp,
|
||||
CONSTRAINT url_uq UNIQUE (url),
|
||||
CONSTRAINT box_pk PRIMARY KEY (id)
|
||||
);
|
||||
-- ddl-end --
|
||||
ALTER TABLE public.box OWNER TO dev;
|
||||
-- ddl-end --
|
||||
|
||||
-- object: id_box | type: COLUMN --
|
||||
ALTER TABLE public.file ADD COLUMN id_box uuid;
|
||||
-- ddl-end --
|
||||
INSERT INTO public.box (id, url, edit_code) VALUES ('ce407978-b599-4cd6-9ddf-e9249280d321', '1', 'foobar');
|
||||
UPDATE public.file SET id_box = 'ce407978-b599-4cd6-9ddf-e9249280d321';
|
||||
ALTER TABLE public.file ALTER COLUMN id_box SET NOT NULL;
|
||||
|
||||
|
||||
-- object: md5 | type: COLUMN --
|
||||
ALTER TABLE public.file ADD COLUMN md5 text;
|
||||
-- ddl-end --
|
||||
UPDATE public.file SET md5 = '';
|
||||
ALTER TABLE public.file ALTER COLUMN md5 SET NOT NULL;
|
||||
|
||||
|
||||
|
||||
|
||||
-- [ Created foreign keys ] --
|
||||
-- object: box_fk | type: CONSTRAINT --
|
||||
-- ALTER TABLE public.file DROP CONSTRAINT IF EXISTS box_fk CASCADE;
|
||||
ALTER TABLE public.file ADD CONSTRAINT box_fk FOREIGN KEY (id_box)
|
||||
REFERENCES public.box (id) MATCH FULL
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
-- ddl-end --
|
||||
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
-- +goose Up
|
||||
-- Diff code generated with pgModeler (PostgreSQL Database Modeler)
|
||||
-- pgModeler version: 1.2.0-alpha1
|
||||
-- Diff date: 2025-04-19 17:49:54
|
||||
-- Source model: dev1
|
||||
-- Database: dev1
|
||||
-- PostgreSQL version: 17.0
|
||||
|
||||
-- [ Diff summary ]
|
||||
-- Dropped objects: 0
|
||||
-- Created objects: 0
|
||||
-- Changed objects: 2
|
||||
|
||||
SET search_path=public,pg_catalog;
|
||||
-- ddl-end --
|
||||
|
||||
|
||||
-- [ Changed objects ] --
|
||||
ALTER TABLE public.file ALTER COLUMN message TYPE varchar(8192);
|
||||
-- ddl-end --
|
||||
ALTER TABLE public.box ALTER COLUMN header TYPE varchar(16384);
|
||||
-- ddl-end --
|
||||
20
database/migrations/20250421015240_box_header_to_text.sql
Normal file
20
database/migrations/20250421015240_box_header_to_text.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
-- +goose Up
|
||||
-- Diff code generated with pgModeler (PostgreSQL Database Modeler)
|
||||
-- pgModeler version: 1.2.0-alpha1
|
||||
-- Diff date: 2025-04-20 22:52:53
|
||||
-- Source model: dev1
|
||||
-- Database: dev1
|
||||
-- PostgreSQL version: 17.0
|
||||
|
||||
-- [ Diff summary ]
|
||||
-- Dropped objects: 0
|
||||
-- Created objects: 0
|
||||
-- Changed objects: 1
|
||||
|
||||
SET search_path=public,pg_catalog;
|
||||
-- ddl-end --
|
||||
|
||||
|
||||
-- [ Changed objects ] --
|
||||
ALTER TABLE public.box ALTER COLUMN header TYPE text;
|
||||
-- ddl-end --
|
||||
47
database/model.go
Normal file
47
database/model.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type Model interface {
|
||||
Id() (int, error)
|
||||
SetId(int)
|
||||
Saved() bool
|
||||
}
|
||||
|
||||
type identity struct {
|
||||
id int
|
||||
saved bool
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNotSaved = fmt.Errorf("primary key id não foi salva")
|
||||
)
|
||||
|
||||
func (p *identity) Id() (int, error) {
|
||||
if !p.saved {
|
||||
return 0, ErrNotSaved
|
||||
}
|
||||
return p.id, nil
|
||||
}
|
||||
|
||||
func (p *identity) SetId(i int) {
|
||||
p.id = i
|
||||
p.saved = true
|
||||
}
|
||||
|
||||
func (p *identity) Saved() bool {
|
||||
return p.saved
|
||||
}
|
||||
|
||||
func checkPasswordHash(password, hash string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
}
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
return string(bytes), err
|
||||
}
|
||||
153
database/pgmodeler/model.dbm
Normal file
153
database/pgmodeler/model.dbm
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
CAUTION: Do not modify this file unless you know what you are doing.
|
||||
Unexpected results may occur if the code is changed deliberately.
|
||||
-->
|
||||
<dbmodel pgmodeler-ver="1.2.0-alpha1" use-changelog="false" max-obj-count="4"
|
||||
last-position="0,29" last-zoom="1"
|
||||
default-schema="public" default-owner="dev"
|
||||
layers="Default layer"
|
||||
active-layers="0"
|
||||
layer-name-colors="#000000"
|
||||
layer-rect-colors="#b4b4b4"
|
||||
show-layer-names="false" show-layer-rects="false">
|
||||
<role name="dev"
|
||||
superuser="true"
|
||||
createdb="true"
|
||||
replication="true"
|
||||
createrole="true"
|
||||
inherit="true"
|
||||
login="true"
|
||||
bypassrls="true"
|
||||
password="********"
|
||||
sql-disabled="true">
|
||||
</role>
|
||||
|
||||
<database name="dev1" encoding="UTF8" lc-collate="en_US.utf8" lc-ctype="en_US.utf8" is-template="false" allow-conns="true">
|
||||
<role name="dev"/>
|
||||
<tablespace name="pg_default"/>
|
||||
</database>
|
||||
|
||||
<schema name="public" layers="0" rect-visible="true" fill-color="#e1e1e1" name-color="#000000" sql-disabled="true">
|
||||
</schema>
|
||||
|
||||
<table name="goose_db_version" layers="0" collapse-mode="2" max-obj-count="4" z-value="0">
|
||||
<schema name="public"/>
|
||||
<role name="dev"/>
|
||||
<position x="440" y="300"/>
|
||||
<column name="id" not-null="true"
|
||||
identity-type="BY DEFAULT" start="1" increment="1" min-value="1" max-value="2147483647" cache="1">
|
||||
<type name="integer" length="0"/>
|
||||
</column>
|
||||
<column name="version_id" not-null="true">
|
||||
<type name="bigint" length="0"/>
|
||||
</column>
|
||||
<column name="is_applied" not-null="true">
|
||||
<type name="boolean" length="0"/>
|
||||
</column>
|
||||
<column name="tstamp" not-null="true" default-value="now()">
|
||||
<type name="timestamp" length="0"/>
|
||||
</column>
|
||||
<constraint name="goose_db_version_pkey" type="pk-constr" table="public.goose_db_version">
|
||||
<columns names="id" ref-type="src-columns"/>
|
||||
</constraint>
|
||||
</table>
|
||||
|
||||
<sequence name="goose_db_version_id_seq" cycle="false" start="1" increment="1" min-value="1" max-value="2147483647" cache="1" sql-disabled="true">
|
||||
<schema name="public"/>
|
||||
<role name="dev"/>
|
||||
</sequence>
|
||||
|
||||
<sequence name="usuario_id_seq" cycle="false" start="1" increment="1" min-value="1" max-value="2147483647" cache="1" sql-disabled="true">
|
||||
<schema name="public"/>
|
||||
<role name="dev"/>
|
||||
</sequence>
|
||||
|
||||
<table name="file" layers="0" collapse-mode="2" max-obj-count="12" z-value="0">
|
||||
<schema name="public"/>
|
||||
<role name="dev"/>
|
||||
<position x="1160" y="520"/>
|
||||
<column name="id" not-null="true">
|
||||
<type name="uuid" length="0"/>
|
||||
</column>
|
||||
<column name="name" not-null="true">
|
||||
<type name="varchar" length="256"/>
|
||||
</column>
|
||||
<column name="size" not-null="true">
|
||||
<type name="integer" length="0"/>
|
||||
</column>
|
||||
<column name="created_at" not-null="true">
|
||||
<type name="timestamp" length="0"/>
|
||||
</column>
|
||||
<column name="mime" not-null="true">
|
||||
<type name="text" length="0"/>
|
||||
</column>
|
||||
<column name="message">
|
||||
<type name="varchar" length="8192"/>
|
||||
</column>
|
||||
<column name="width">
|
||||
<type name="smallint" length="0"/>
|
||||
</column>
|
||||
<column name="height">
|
||||
<type name="smallint" length="0"/>
|
||||
</column>
|
||||
<column name="md5" not-null="true">
|
||||
<type name="text" length="0"/>
|
||||
</column>
|
||||
<constraint name="file_pk" type="pk-constr" table="public.file">
|
||||
<columns names="id" ref-type="src-columns"/>
|
||||
</constraint>
|
||||
|
||||
<customidxs object-type="column">
|
||||
<object name="id_box" index="8"/>
|
||||
</customidxs>
|
||||
<customidxs object-type="constraint">
|
||||
<object name="box_fk" index="1"/>
|
||||
</customidxs></table>
|
||||
|
||||
<table name="box" layers="0" collapse-mode="2" max-obj-count="9" z-value="0">
|
||||
<schema name="public"/>
|
||||
<role name="dev"/>
|
||||
<position x="560" y="560"/>
|
||||
<column name="id" not-null="true">
|
||||
<type name="uuid" length="0"/>
|
||||
</column>
|
||||
<column name="url" not-null="true">
|
||||
<type name="varchar" length="256"/>
|
||||
</column>
|
||||
<column name="edit_code" not-null="true">
|
||||
<type name="varchar" length="64"/>
|
||||
</column>
|
||||
<column name="header">
|
||||
<type name="text" length="16384"/>
|
||||
</column>
|
||||
<column name="private" not-null="true" default-value="false">
|
||||
<type name="boolean" length="0"/>
|
||||
</column>
|
||||
<column name="moderation" not-null="true" default-value="false">
|
||||
<type name="boolean" length="0"/>
|
||||
</column>
|
||||
<column name="created_at" not-null="true" default-value="current_timestamp">
|
||||
<type name="timestamp" length="0"/>
|
||||
</column>
|
||||
<column name="last_updated_at" not-null="true" default-value="current_timestamp">
|
||||
<type name="timestamp" length="0"/>
|
||||
</column>
|
||||
<constraint name="url_uq" type="uq-constr" table="public.box">
|
||||
<columns names="url" ref-type="src-columns"/>
|
||||
</constraint>
|
||||
<constraint name="box_pk" type="pk-constr" table="public.box">
|
||||
<columns names="id" ref-type="src-columns"/>
|
||||
</constraint>
|
||||
</table>
|
||||
|
||||
<relationship name="box_has_many_file" type="rel1n" layers="0"
|
||||
src-col-pattern="{sc}_{st}"
|
||||
pk-pattern="{dt}_pk" uq-pattern="{dt}_uq"
|
||||
src-fk-pattern="{st}_fk"
|
||||
custom-color="#ccd394"
|
||||
src-table="public.box"
|
||||
dst-table="public.file"
|
||||
src-required="true" dst-required="false"/>
|
||||
|
||||
</dbmodel>
|
||||
69
database/usuario.go
Normal file
69
database/usuario.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Usuario struct {
|
||||
identity
|
||||
Email string
|
||||
Nome string
|
||||
// senhaHash string `json:"-"`
|
||||
Ctime time.Time
|
||||
}
|
||||
|
||||
func VerifyUser(tx *sql.Tx, email, senha string) (*Usuario, error) {
|
||||
row := tx.QueryRow(
|
||||
`SELECT id, nome, ctime, senha_hash FROM usuario WHERE email = $1 LIMIT 1`,
|
||||
email,
|
||||
)
|
||||
if row.Err() != nil {
|
||||
return nil, row.Err()
|
||||
}
|
||||
usuario := &Usuario{
|
||||
Email: email,
|
||||
}
|
||||
|
||||
var id int
|
||||
var senhaHash string
|
||||
err := row.Scan(
|
||||
&id,
|
||||
&usuario.Nome,
|
||||
&usuario.Ctime,
|
||||
&senhaHash,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usuario.SetId(id)
|
||||
|
||||
if err = checkPasswordHash(senha, senhaHash); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return usuario, nil
|
||||
}
|
||||
|
||||
func FindUsuarioByID(tx *sql.Tx, id int) (*Usuario, error) {
|
||||
row := tx.QueryRow(
|
||||
`SELECT id, nome, email, ctime FROM usuario WHERE id = $1 LIMIT 1`,
|
||||
id,
|
||||
)
|
||||
if row.Err() != nil {
|
||||
return nil, row.Err()
|
||||
}
|
||||
usuario := &Usuario{}
|
||||
err := row.Scan(
|
||||
&id,
|
||||
&usuario.Nome,
|
||||
&usuario.Email,
|
||||
&usuario.Ctime,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usuario.SetId(id)
|
||||
return usuario, nil
|
||||
}
|
||||
73
docker-compose.yml
Normal file
73
docker-compose.yml
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
|
||||
networks:
|
||||
metrics:
|
||||
driver: bridge
|
||||
transactional:
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres
|
||||
healthcheck:
|
||||
# Serve de condicional para que o docker compose saiba que o banco
|
||||
# foi criado com sucesso antes de inicializar a aplicação.
|
||||
test: ["CMD-SHELL", "pg_isready -U ${PG_USER} -d ${PG_DB}"]
|
||||
interval: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
timeout: 10s
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
|
||||
environment:
|
||||
POSTGRES_USER: ${PG_USER}
|
||||
POSTGRES_PASSWORD: ${PG_PASS}
|
||||
POSTGRES_DB: ${PG_DB}
|
||||
ports:
|
||||
- 5432:5432
|
||||
networks:
|
||||
- transactional
|
||||
|
||||
# Interface web para monitoramento e consultas no banco.
|
||||
adminer:
|
||||
image: adminer
|
||||
ports:
|
||||
- 8090:8080
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
- transactional
|
||||
|
||||
# Observabilidade e coleta de dados
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
expose:
|
||||
- '9090'
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
networks:
|
||||
- metrics
|
||||
# Necessário para que o serviço consiga encontrar a aplicação quando
|
||||
# estivermos desenvolvendo localmente, fora de um contêiner.
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
# Servidor. rodar apenas quando estivermos em produção.
|
||||
app:
|
||||
build: .
|
||||
environment:
|
||||
# Aplicação não se conecta com o banco sem isso.
|
||||
# Apenas funciona pois o docker compose registra o nome de cada serviço como um
|
||||
# endereço de rede, e esse é o endereço do banco.
|
||||
PG_ADDR: db
|
||||
depends_on:
|
||||
db: # Não incializa até que o banco esteja recebendo requisições
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
ports:
|
||||
- 8888:8888
|
||||
networks:
|
||||
- metrics
|
||||
- transactional
|
||||
53
go.mod
Normal file
53
go.mod
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
module foobar
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/jackc/pgx/v5 v5.7.2
|
||||
github.com/joho/godotenv v1.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.10.1 // indirect
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
||||
github.com/a-h/templ v0.3.833 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cli/browser v1.3.0 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.4.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/natefinch/atomic v1.0.1 // indirect
|
||||
github.com/pressly/goose/v3 v3.24.1 // indirect
|
||||
github.com/prometheus/client_golang v1.21.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
)
|
||||
166
go.sum
Normal file
166
go.sum
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
|
||||
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
||||
github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU=
|
||||
github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
|
||||
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
|
||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
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/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
|
||||
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
|
||||
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pressly/goose/v3 v3.24.1 h1:bZmxRco2uy5uu5Ng1MMVEfYsFlrMJI+e/VMXHQ3C4LY=
|
||||
github.com/pressly/goose/v3 v3.24.1/go.mod h1:rEWreU9uVtt0DHCyLzF9gRcWiiTF/V+528DV+4DORug=
|
||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
46
metrics/main.go
Normal file
46
metrics/main.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
responseLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: "response_latency",
|
||||
Help: "latency of the response",
|
||||
},
|
||||
// favor adicionar itens à essa lista com cautela.
|
||||
// uma alta cardinalidade em vetores de uma métrica no prometheus
|
||||
// pode causar impactos na performance do servidor remoto.
|
||||
[]string{"status", "method", "path", "panic"},
|
||||
)
|
||||
|
||||
TransactionLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: "transaction_db_latency",
|
||||
Help: "latency of the response",
|
||||
}, []string{})
|
||||
)
|
||||
|
||||
func ResponseLatencyObserve(latency time.Duration, statusCode int, panicked bool, r *http.Request) {
|
||||
var (
|
||||
method = r.Method
|
||||
path = r.URL.Path
|
||||
)
|
||||
observer, err := responseLatency.GetMetricWithLabelValues(
|
||||
strconv.Itoa(statusCode),
|
||||
method,
|
||||
path,
|
||||
fmt.Sprint(panicked),
|
||||
)
|
||||
if err != nil {
|
||||
// não temos como tratar esse erro
|
||||
panic(err)
|
||||
}
|
||||
observer.Observe(latency.Seconds())
|
||||
}
|
||||
239
model/main.go
Normal file
239
model/main.go
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
// Algumas dessas classes sao absurdas mas quero ver até onde isso vai.
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/textproto"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"crypto/md5"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Filename string
|
||||
|
||||
type BoxURL struct {
|
||||
url string
|
||||
}
|
||||
|
||||
var boxURLCheck = regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
|
||||
|
||||
func CheckBoxURL(url string) (BoxURL, error) {
|
||||
if url == "" || !boxURLCheck.MatchString(url) {
|
||||
return BoxURL{}, fmt.Errorf(`invalid url: "%s"`, url)
|
||||
}
|
||||
return BoxURL{url}, nil
|
||||
}
|
||||
|
||||
func (b BoxURL) String() string {
|
||||
return b.url
|
||||
}
|
||||
|
||||
type FileMime string
|
||||
|
||||
type Markdown string
|
||||
|
||||
type MD5Checksum [md5.Size]byte
|
||||
|
||||
func (m MD5Checksum) String() string {
|
||||
return fmt.Sprintf("%x", m[:])
|
||||
}
|
||||
|
||||
type File struct {
|
||||
id uuid.UUID
|
||||
name string
|
||||
size int64
|
||||
createdAt time.Time
|
||||
mime FileMime
|
||||
checksum MD5Checksum
|
||||
}
|
||||
|
||||
func NewFile(
|
||||
id uuid.UUID,
|
||||
name Filename,
|
||||
size int64,
|
||||
createdat time.Time,
|
||||
mime FileMime,
|
||||
checksum MD5Checksum,
|
||||
) File {
|
||||
return File{
|
||||
id: id,
|
||||
name: string(name),
|
||||
size: size,
|
||||
createdAt: createdat,
|
||||
mime: mime,
|
||||
checksum: checksum,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *File) ID() uuid.UUID {
|
||||
return f.id
|
||||
}
|
||||
|
||||
func (f *File) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f *File) Size() int64 {
|
||||
return f.size
|
||||
}
|
||||
|
||||
func (f *File) CreatedAt() time.Time {
|
||||
return f.createdAt
|
||||
}
|
||||
|
||||
func (f *File) Mime() FileMime {
|
||||
return f.mime
|
||||
}
|
||||
|
||||
func (f *File) Checksum() MD5Checksum {
|
||||
return f.checksum
|
||||
}
|
||||
|
||||
type FileUploadContext struct {
|
||||
id uuid.UUID
|
||||
uploadedDate time.Time
|
||||
size int64
|
||||
filename Filename
|
||||
mime FileMime
|
||||
fileHeader *multipart.FileHeader
|
||||
}
|
||||
|
||||
func NewFileUploadContext(
|
||||
id uuid.UUID,
|
||||
uploadedDate time.Time,
|
||||
size int64,
|
||||
filename Filename,
|
||||
mime FileMime,
|
||||
fileHeader *multipart.FileHeader,
|
||||
) FileUploadContext {
|
||||
|
||||
return FileUploadContext{
|
||||
id: id,
|
||||
uploadedDate: uploadedDate,
|
||||
size: size,
|
||||
filename: filename,
|
||||
mime: mime,
|
||||
fileHeader: fileHeader,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileUploadContext) ID() uuid.UUID {
|
||||
return f.id
|
||||
}
|
||||
|
||||
func (f *FileUploadContext) UploadedDate() time.Time {
|
||||
return f.uploadedDate
|
||||
}
|
||||
|
||||
func (f *FileUploadContext) Size() int64 {
|
||||
return f.size
|
||||
}
|
||||
|
||||
func (f *FileUploadContext) Filename() Filename {
|
||||
return f.filename
|
||||
}
|
||||
|
||||
func (f *FileUploadContext) Mime() FileMime {
|
||||
return f.mime
|
||||
}
|
||||
|
||||
func (f *FileUploadContext) FileHeader() *multipart.FileHeader {
|
||||
return f.fileHeader
|
||||
}
|
||||
|
||||
type Box struct {
|
||||
id uuid.UUID
|
||||
url BoxURL
|
||||
header Markdown
|
||||
private bool
|
||||
moderation bool
|
||||
createdAt time.Time
|
||||
lastUpdatedAt time.Time
|
||||
|
||||
// FIXME: should be encoded in bcrypt
|
||||
editCode string
|
||||
}
|
||||
|
||||
func NewBox(
|
||||
id uuid.UUID,
|
||||
url BoxURL,
|
||||
editCode string,
|
||||
header Markdown,
|
||||
private, moderation bool,
|
||||
createdAt, lastUpdatedAt time.Time,
|
||||
) Box {
|
||||
return Box{
|
||||
id: id,
|
||||
url: url,
|
||||
header: header,
|
||||
editCode: editCode,
|
||||
private: private,
|
||||
moderation: moderation,
|
||||
createdAt: createdAt,
|
||||
lastUpdatedAt: lastUpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (b Box) ID() uuid.UUID {
|
||||
return b.id
|
||||
}
|
||||
|
||||
func (b Box) Url() BoxURL {
|
||||
return b.url
|
||||
}
|
||||
|
||||
func (b Box) Header() Markdown {
|
||||
return b.header
|
||||
}
|
||||
|
||||
func (b Box) EditCode() string {
|
||||
return b.editCode
|
||||
}
|
||||
|
||||
func (b Box) Private() bool {
|
||||
return b.private
|
||||
}
|
||||
|
||||
func (b Box) Moderation() bool {
|
||||
return b.moderation
|
||||
}
|
||||
|
||||
func (b Box) CreatedAt() time.Time {
|
||||
return b.createdAt
|
||||
}
|
||||
|
||||
func (b Box) LastUpdatedAt() time.Time {
|
||||
return b.lastUpdatedAt
|
||||
}
|
||||
|
||||
func (m FileMime) IsImage() bool {
|
||||
return strings.Contains(string(m), "image")
|
||||
}
|
||||
|
||||
func (m FileMime) IsVideo() bool {
|
||||
return strings.Contains(string(m), "video")
|
||||
}
|
||||
|
||||
func (m FileMime) PrettyType() string {
|
||||
switch {
|
||||
case strings.Contains(string(m), "gif"):
|
||||
return "gif"
|
||||
case m.IsImage():
|
||||
return "image"
|
||||
case m.IsVideo():
|
||||
return "video"
|
||||
case strings.Contains(string(m), "pdf"):
|
||||
return "pdf"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func MimeFromHeader(header textproto.MIMEHeader) FileMime {
|
||||
return FileMime(header.Get("Content-Type"))
|
||||
}
|
||||
21
model/main_test.go
Normal file
21
model/main_test.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func pp(result, expected any) error {
|
||||
return fmt.Errorf("result: %v, expected: %v", result, expected)
|
||||
}
|
||||
|
||||
func TestCheckBoxURL(t *testing.T) {
|
||||
boxURL, err := CheckBoxURL("123123")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if result, expected := fmt.Sprintf("%s", boxURL), "123123"; result != expected {
|
||||
t.Fatal(pp(result, expected))
|
||||
}
|
||||
}
|
||||
23
prometheus/prometheus.yml
Normal file
23
prometheus/prometheus.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
global:
|
||||
scrape_interval: 15s # By default, scrape targets every 15 seconds.
|
||||
|
||||
# Attach these labels to any time series or alerts when communicating with
|
||||
# external systems (federation, remote storage, Alertmanager).
|
||||
external_labels:
|
||||
monitor: 'codelab-monitor'
|
||||
|
||||
# A scrape configuration containing exactly one endpoint to scrape:
|
||||
# Here it's Prometheus itself.
|
||||
scrape_configs:
|
||||
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
|
||||
- job_name: 'prometheus'
|
||||
scrape_interval: 5s
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
- job_name: 'node'
|
||||
scrape_interval: 5s
|
||||
static_configs:
|
||||
- targets: ['host.docker.internal:8888']
|
||||
labels:
|
||||
group: 'production'
|
||||
69
routes/auth.go
Normal file
69
routes/auth.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"foobar/database"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/joho/godotenv" // Carrega .env
|
||||
)
|
||||
|
||||
var store *sessions.CookieStore
|
||||
|
||||
const sessionIdCookie = "session_id"
|
||||
|
||||
func init() {
|
||||
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
key := os.Getenv("SESSION_STORE_KEY")
|
||||
|
||||
if key == "" {
|
||||
panic(fmt.Errorf("chave da sessão de cookies não foi definida."))
|
||||
}
|
||||
|
||||
store = sessions.NewCookieStore([]byte(key))
|
||||
}
|
||||
|
||||
func currentUser(r *http.Request) (*database.Usuario, error) {
|
||||
session, err := store.Get(r, sessionIdCookie)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, ok := session.Values["userId"].(int)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not ok")
|
||||
}
|
||||
|
||||
tx := database.MustBeginTx()
|
||||
defer tx.Rollback()
|
||||
|
||||
usuario, err := database.FindUsuarioByID(tx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return usuario, nil
|
||||
}
|
||||
|
||||
func redirectUnauthorized(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := store.Get(r, sessionIdCookie)
|
||||
if err != nil {
|
||||
logInternalError(w, err)
|
||||
return
|
||||
}
|
||||
session.AddFlash("Autenticação necessária.")
|
||||
err = session.Save(r, w)
|
||||
if err != nil {
|
||||
logInternalError(w, err)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/index.html", http.StatusFound)
|
||||
}
|
||||
69
routes/box.go
Normal file
69
routes/box.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"foobar/database"
|
||||
"foobar/model"
|
||||
"foobar/views"
|
||||
"net/http"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
func boxUpload(w http.ResponseWriter, r *http.Request) (templ.Component, error) {
|
||||
boxURL, err := GetBoxURL(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
box, err := database.SelectBox(boxURL)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return views.NewBox(boxURL, readmeTemplate), nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return views.BoxUpload(box), nil
|
||||
}
|
||||
|
||||
func checkBox(w http.ResponseWriter, r *http.Request) (templ.Component, error) {
|
||||
boxURL, err := GetBoxURL(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
box, err := database.SelectBox(boxURL)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return views.Box(boxURL, []model.File{}), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
boxFiles, err := database.SelectBoxFiles(box.ID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return views.Box(boxURL, boxFiles), nil
|
||||
}
|
||||
|
||||
func mdToHTML(md []byte) model.Markdown {
|
||||
// create markdown parser with extensions
|
||||
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
|
||||
p := parser.NewWithExtensions(extensions)
|
||||
doc := p.Parse(md)
|
||||
|
||||
// create HTML renderer with extensions
|
||||
htmlFlags := html.CommonFlags | html.HrefTargetBlank
|
||||
opts := html.RendererOptions{Flags: htmlFlags}
|
||||
renderer := html.NewRenderer(opts)
|
||||
|
||||
sanitizer := bluemonday.UGCPolicy()
|
||||
return model.Markdown(sanitizer.SanitizeBytes(markdown.Render(doc, renderer)))
|
||||
}
|
||||
204
routes/file.go
Normal file
204
routes/file.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"foobar/database"
|
||||
"foobar/model"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var maxFileSize int64 = 32 * 1000 * 1000
|
||||
|
||||
func fileUpload(w http.ResponseWriter, r *http.Request) (redirectURL, error) {
|
||||
|
||||
boxURL, err := GetBoxURL(r)
|
||||
if err != nil {
|
||||
return noRedirect, err
|
||||
}
|
||||
|
||||
tx := database.MustBeginTx()
|
||||
defer tx.Rollback()
|
||||
|
||||
box, err := database.SelectBox(boxURL)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) { // this is a new URL the user wants to claim for themselves
|
||||
editCode := r.FormValue("edit_code")
|
||||
if editCode == "" {
|
||||
return noRedirect, newUserError(
|
||||
"Missing edit code. Box can't be created without it",
|
||||
)
|
||||
}
|
||||
var (
|
||||
id = uuid.New()
|
||||
createdAt = time.Now()
|
||||
lastUpdatedAt = createdAt
|
||||
header = readmeTemplate
|
||||
)
|
||||
newBox := model.NewBox(
|
||||
id, boxURL, editCode, header, false, false, createdAt, lastUpdatedAt,
|
||||
)
|
||||
err = database.InsertBox(tx, newBox, editCode, header)
|
||||
if err != nil {
|
||||
return noRedirect, err
|
||||
}
|
||||
box = newBox
|
||||
} else {
|
||||
return noRedirect, err
|
||||
}
|
||||
}
|
||||
|
||||
filesCtx, err := GetFilesFromUploadRequest(r)
|
||||
if err != nil {
|
||||
return noRedirect, err
|
||||
}
|
||||
|
||||
var files []model.File
|
||||
|
||||
for _, file := range filesCtx {
|
||||
|
||||
bytes, err := readFileBytes(file.FileHeader())
|
||||
if err != nil {
|
||||
return noRedirect, err
|
||||
}
|
||||
|
||||
mime := model.FileMime(http.DetectContentType(bytes))
|
||||
|
||||
if file.Filename() == "README.md" {
|
||||
|
||||
editCode := r.FormValue("edit_code")
|
||||
if editCode == "" {
|
||||
return noRedirect, newUserError(
|
||||
"Missing edit code. README can't be updated. " +
|
||||
"If you're not attempting to update the header, we'd " +
|
||||
"suggest changing \"README.md\" to a new filename.",
|
||||
)
|
||||
}
|
||||
|
||||
if editCode != box.EditCode() {
|
||||
return noRedirect, newUserError(
|
||||
"Wrong edit code.",
|
||||
)
|
||||
}
|
||||
|
||||
md := mdToHTML(bytes)
|
||||
|
||||
err := database.UpdateHeader(tx, box.ID(), md)
|
||||
if err != nil {
|
||||
return noRedirect, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return noRedirect, err
|
||||
}
|
||||
|
||||
// FIXME: we're ignoring all other uploaded files
|
||||
return redirectURL(fmt.Sprintf("/box/%s", box.Url())), nil
|
||||
|
||||
} else {
|
||||
|
||||
err = copyBytesToDisk(file.ID(), bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
checksum := calculateMD5(bytes)
|
||||
|
||||
newFile := model.NewFile(
|
||||
file.ID(), file.Filename(), file.Size(),
|
||||
file.UploadedDate(), mime, checksum,
|
||||
)
|
||||
|
||||
files = append(files, newFile)
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
err = database.InsertFile(tx, box.ID(), file)
|
||||
if err != nil {
|
||||
return noRedirect, err
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return noRedirect, err
|
||||
}
|
||||
|
||||
return redirectURL(fmt.Sprintf("/box/%s/inside", box.Url())), nil
|
||||
}
|
||||
|
||||
func calculateMD5(fileBytes []byte) model.MD5Checksum {
|
||||
return md5.Sum(fileBytes)
|
||||
}
|
||||
|
||||
func readFileBytes(fileHeader *multipart.FileHeader) ([]byte, error) {
|
||||
fileReader, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer fileReader.Close()
|
||||
|
||||
fileBytes, err := io.ReadAll(fileReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileBytes, nil
|
||||
}
|
||||
|
||||
func copyBytesToDisk(fileId uuid.UUID, fileBytes []byte) error {
|
||||
fileWriter, err := os.Create(filepath.Join(
|
||||
staticDir, "box", fmt.Sprintf(fileId.String())),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fileWriter.Close()
|
||||
|
||||
_, err = io.Copy(fileWriter, bytes.NewReader(fileBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetFilesFromUploadRequest(r *http.Request) ([]model.FileUploadContext, error) {
|
||||
|
||||
err := r.ParseMultipartForm(int64(maxFileSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
headers, ok := r.MultipartForm.File["file"]
|
||||
if !ok {
|
||||
return nil, newUserError("no file")
|
||||
}
|
||||
|
||||
var (
|
||||
files []model.FileUploadContext
|
||||
)
|
||||
|
||||
for _, header := range headers {
|
||||
var (
|
||||
id = uuid.New()
|
||||
uploadedDate = time.Now()
|
||||
size = header.Size
|
||||
filename = model.Filename(header.Filename)
|
||||
mime = model.MimeFromHeader(header.Header)
|
||||
)
|
||||
|
||||
fileUpload := model.NewFileUploadContext(id, uploadedDate, size, filename, mime, header)
|
||||
files = append(files, fileUpload)
|
||||
}
|
||||
return files, nil
|
||||
|
||||
}
|
||||
97
routes/logging.go
Normal file
97
routes/logging.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"foobar/metrics"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
type wrappedWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (w *wrappedWriter) WriteHeader(statusCode int) {
|
||||
w.ResponseWriter.WriteHeader(statusCode)
|
||||
w.statusCode = statusCode
|
||||
}
|
||||
|
||||
func logging(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
wrapped := &wrappedWriter{
|
||||
ResponseWriter: w,
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
|
||||
var panicked bool
|
||||
err := recoverServeHTTP(next, wrapped, r)
|
||||
if err != nil {
|
||||
wrapped.Write([]byte("500 error"))
|
||||
log.Println(err)
|
||||
panicked = true
|
||||
}
|
||||
|
||||
var (
|
||||
latency = time.Since(start)
|
||||
path = r.URL.Path
|
||||
statusCode = wrapped.statusCode
|
||||
)
|
||||
if path == "/metrics" && statusCode == 200 {
|
||||
return
|
||||
}
|
||||
log.Printf("%v %v %v %v", wrapped.statusCode, r.Method, r.URL.Path, latency)
|
||||
|
||||
metrics.ResponseLatencyObserve(latency, statusCode, panicked, r)
|
||||
})
|
||||
}
|
||||
|
||||
func recoverServeHTTP(next http.Handler, w http.ResponseWriter, r *http.Request) (e error) {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
var err error
|
||||
switch v := rec.(type) {
|
||||
case error:
|
||||
err = v
|
||||
default:
|
||||
err = fmt.Errorf("%v", v)
|
||||
}
|
||||
stack := debug.Stack()
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
e = errors.Join(err, fmt.Errorf(string(stack)))
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
func logError(err error) {
|
||||
if err == nil {
|
||||
log.Printf("LOG: Erro Nulo!?\n")
|
||||
} else {
|
||||
log.Printf("LOG: %s\n", err.Error())
|
||||
}
|
||||
log.Println(string(debug.Stack()))
|
||||
}
|
||||
|
||||
func logInternalError(w http.ResponseWriter, err error) {
|
||||
logError(err)
|
||||
http.Error(w, `500`, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func httpErrorf(w http.ResponseWriter, status int, formatString string, format ...interface{}) {
|
||||
http.Error(w, fmt.Sprintf(formatString, format...), status)
|
||||
}
|
||||
|
||||
func formValueMissing(field string, r *http.Request) error {
|
||||
return fmt.Errorf(
|
||||
"campo '%s' em formulário não pôde ser encontrado. Content-Type: %s",
|
||||
field,
|
||||
r.Header.Get("Content-Type"),
|
||||
)
|
||||
}
|
||||
97
routes/middleware.go
Normal file
97
routes/middleware.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"foobar/views"
|
||||
"net/http"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
type userError struct {
|
||||
error
|
||||
user string
|
||||
}
|
||||
|
||||
type redirectURL string
|
||||
|
||||
type postRouteRedirectFunc func(http.ResponseWriter, *http.Request) (redirectURL, error)
|
||||
|
||||
type routeGetFunc func(http.ResponseWriter, *http.Request) (templ.Component, error)
|
||||
|
||||
var (
|
||||
errUser = errors.New("user error")
|
||||
)
|
||||
|
||||
func newUserError(msg string) *userError {
|
||||
return &userError{
|
||||
user: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *userError) Error() string {
|
||||
return u.user
|
||||
}
|
||||
|
||||
func (u redirectURL) Valid() bool {
|
||||
return len(u) > 0 && u[0] == '/'
|
||||
}
|
||||
|
||||
func getRouteMiddleware(fun routeGetFunc) http.HandlerFunc {
|
||||
// TODO: handle boosted HTMX requests gracefully
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
component, err := fun(w, r)
|
||||
if err != nil {
|
||||
logInternalError(w, err)
|
||||
return
|
||||
}
|
||||
err = component.Render(context.Background(), w)
|
||||
if err != nil {
|
||||
logInternalError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func redirectHtmxFormMiddleware(fun postRouteRedirectFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
hxRequest := r.Header.Get("HX-Request") == "true"
|
||||
|
||||
redirectURL, err := fun(w, r)
|
||||
if err != nil {
|
||||
w.Header().Add("HX-Retarget", "#error-target")
|
||||
w.Header().Add("HX-Reswap", "innerHTML")
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
if u, ok := err.(*userError); ok {
|
||||
if hxRequest {
|
||||
err = views.ErrorBox(u.user).Render(context.Background(), w)
|
||||
if err != nil {
|
||||
logError(fmt.Errorf("%w: %s", err, r.URL.Path))
|
||||
}
|
||||
} else {
|
||||
_, _ = w.Write([]byte(u.user))
|
||||
|
||||
}
|
||||
return
|
||||
} else {
|
||||
msg := "Internal error... See the server logs for more information"
|
||||
_ = views.ErrorBox(msg).Render(context.Background(), w)
|
||||
logError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !redirectURL.Valid() {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
if hxRequest {
|
||||
w.Header().Add("HX-Redirect", string(redirectURL))
|
||||
} else {
|
||||
http.Redirect(w, r, string(redirectURL), http.StatusFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
178
routes/routes.go
Normal file
178
routes/routes.go
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"foobar/database"
|
||||
"foobar/model"
|
||||
"foobar/views"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const noRedirect redirectURL = ""
|
||||
|
||||
var (
|
||||
mux *http.ServeMux = nil
|
||||
errBoxNotFound error = fmt.Errorf("box id not found")
|
||||
staticDir string = filepath.Join("views", "static")
|
||||
)
|
||||
|
||||
var readmeTemplate model.Markdown
|
||||
|
||||
func init() {
|
||||
mux = http.NewServeMux()
|
||||
if err := os.MkdirAll(staticDir, 777); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServerFS(os.DirFS(staticDir))))
|
||||
|
||||
mux.HandleFunc("/", index)
|
||||
mux.HandleFunc("POST /box/{box}", redirectHtmxFormMiddleware(fileUpload))
|
||||
mux.HandleFunc("GET /box/{box}", getRouteMiddleware(boxUpload))
|
||||
mux.HandleFunc("GET /box/{box}/inside", getRouteMiddleware(checkBox))
|
||||
|
||||
mux.HandleFunc("/panic", func(w http.ResponseWriter, r *http.Request) {
|
||||
panic("não implementado")
|
||||
})
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := template.Must(template.ParseFiles(filepath.Join(staticDir, "default", "README.md"))).
|
||||
Execute(&buf, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
readmeTemplate = mdToHTML(buf.Bytes())
|
||||
}
|
||||
|
||||
func Mux() http.Handler {
|
||||
return logging(mux)
|
||||
}
|
||||
|
||||
func notFound(w http.ResponseWriter) {
|
||||
w.WriteHeader(404)
|
||||
if err := views.FourOfour().Render(context.Background(), w); err != nil {
|
||||
logInternalError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func index(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/index.html" && r.URL.Path != "/" || r.Method != "GET" {
|
||||
notFound(w)
|
||||
return
|
||||
}
|
||||
if err := views.Index().Render(context.Background(), w); err != nil {
|
||||
logInternalError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func GetBoxURL(r *http.Request) (model.BoxURL, error) {
|
||||
return model.CheckBoxURL(r.PathValue("box"))
|
||||
}
|
||||
|
||||
// func sobre(w http.ResponseWriter, r *http.Request) {
|
||||
// if err := views.Sobre().Render(context.Background(), w); err != nil {
|
||||
// logInternalError(w, err)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
// func logIn(w http.ResponseWriter, r *http.Request) {
|
||||
//
|
||||
// _, err := currentUser(r)
|
||||
//
|
||||
// userLogged := err == nil
|
||||
//
|
||||
// if userLogged {
|
||||
// http.Redirect(w, r, "/logged", http.StatusFound)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if err := views.Login().Render(context.Background(), w); err != nil {
|
||||
// logInternalError(w, err)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
func logInPOST(w http.ResponseWriter, r *http.Request) (redirectURL, error) {
|
||||
|
||||
email := r.FormValue("email")
|
||||
if email == "" {
|
||||
return "", formValueMissing("email", r)
|
||||
}
|
||||
|
||||
senha := r.FormValue("senha")
|
||||
if senha == "" {
|
||||
return "", formValueMissing("senha", r)
|
||||
}
|
||||
|
||||
tx := database.MustBeginTx()
|
||||
defer tx.Rollback()
|
||||
|
||||
usuario, err := database.VerifyUser(tx, email, senha)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) || errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
||||
return "", newUserError("senha ou email não conferem")
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
session, err := store.Get(r, sessionIdCookie)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
userId, err := usuario.Id()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
session.Values["userId"] = userId
|
||||
session.Options.SameSite = http.SameSiteDefaultMode
|
||||
|
||||
if err := session.Save(r, w); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "/logged", nil
|
||||
}
|
||||
|
||||
func logged(w http.ResponseWriter, r *http.Request) {
|
||||
usuario, err := currentUser(r)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte("Não Autorizado."))
|
||||
return
|
||||
}
|
||||
if err = views.Logged(usuario).Render(context.Background(), w); err != nil {
|
||||
logInternalError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func logout(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := store.Get(r, sessionIdCookie)
|
||||
if err != nil {
|
||||
logInternalError(w, err)
|
||||
return
|
||||
}
|
||||
session.Options.MaxAge = -1
|
||||
if err = session.Save(r, w); err != nil {
|
||||
logInternalError(w, err)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
}
|
||||
79
server/server.go
Normal file
79
server/server.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"foobar/database"
|
||||
"foobar/routes"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv" // Carrega .env
|
||||
)
|
||||
|
||||
//go:generate templ generate
|
||||
|
||||
var (
|
||||
Port = "8888"
|
||||
Addr = "localhost"
|
||||
s *http.Server = nil
|
||||
)
|
||||
|
||||
func MustStartServer() {
|
||||
if s != nil {
|
||||
return
|
||||
}
|
||||
ready := make(chan bool, 1)
|
||||
go mustStartServer(ready)
|
||||
<-ready
|
||||
}
|
||||
|
||||
func mustStartServer(ready chan<- bool) {
|
||||
|
||||
if s != nil {
|
||||
ready <- true
|
||||
return
|
||||
}
|
||||
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if port := os.Getenv("SERVER_PORT"); port != "" {
|
||||
Port = port
|
||||
}
|
||||
if addr := os.Getenv("SERVER_ADDR"); addr != "" {
|
||||
Addr = addr
|
||||
}
|
||||
|
||||
_ = database.New()
|
||||
routes := routes.Mux()
|
||||
|
||||
s = &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%s", Addr, Port),
|
||||
Handler: routes,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
}
|
||||
|
||||
// Abre uma conexão de socket mas não bloqueia o fluxo do programa.
|
||||
// Isso significa que o servidor já está recebendo requests.
|
||||
// Isso é necessário para rodar os testes.
|
||||
ln, err := net.Listen("tcp", s.Addr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ready <- true
|
||||
fmt.Printf("Estamos ao vivo! teste em http://%s:%s\n", Addr, Port)
|
||||
|
||||
log.Fatal(s.Serve(ln))
|
||||
}
|
||||
|
||||
func main() {
|
||||
mustStartServer(make(chan bool, 1))
|
||||
}
|
||||
11
tailwind.config.js
Normal file
11
tailwind.config.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
// "./internal/**/*.{go,js,templ,html}",
|
||||
"./views/html/*.html"
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
1228
views/static/css/github-markdown.css
Normal file
1228
views/static/css/github-markdown.css
Normal file
File diff suppressed because it is too large
Load diff
78
views/static/css/input.css
Normal file
78
views/static/css/input.css
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
/* não deve ser o jeito certo de fazer isso. */
|
||||
|
||||
.primary_button {
|
||||
@apply rounded p-2 bg-indigo-500;
|
||||
}
|
||||
|
||||
.my-input {
|
||||
@apply w-full bg-transparent placeholder:text-slate-400 text-slate-700 text-sm border border-slate-200 rounded-md px-3 py-2 transition duration-300 focus:outline-none focus:border-slate-400 hover:border-slate-300 shadow-sm focus:shadow;
|
||||
}
|
||||
|
||||
.my-label {
|
||||
@apply block mb-1 text-sm text-slate-600;
|
||||
}
|
||||
|
||||
.however {
|
||||
@apply flex items-center mt-2 text-xs text-slate-400;
|
||||
}
|
||||
|
||||
.my-my-input {
|
||||
@apply w-full min-w-[200px] mb-5;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply w-full rounded-md bg-slate-800 py-2 px-4 my-4 border border-transparent text-center text-sm text-white transition-all shadow-md hover:shadow-lg focus:bg-slate-700 focus:shadow-none active:bg-slate-700 hover:bg-slate-700 active:shadow-none disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-5xl font-bold mb-5
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-4xl font-bold mb-5
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@keyframes bounce {
|
||||
0% {
|
||||
transform: translateX(-7px);
|
||||
timing-function: ease-in;
|
||||
}
|
||||
37% {
|
||||
transform: translateX(7px);
|
||||
timing-function: ease-out;
|
||||
}
|
||||
55% {
|
||||
transform: translateX(-7px);
|
||||
timing-function: ease-in;
|
||||
}
|
||||
73% {
|
||||
transform: translateX(5px);
|
||||
timing-function: ease-out;
|
||||
}
|
||||
82% {
|
||||
transform: translateX(-5px);
|
||||
timing-function: ease-in;
|
||||
}
|
||||
91% {
|
||||
transform: translateX(2px);
|
||||
timing-function: ease-out;
|
||||
}
|
||||
96% {
|
||||
transform: translateX(-2px);
|
||||
timing-function: ease-in;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0px);
|
||||
timing-function: ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
.bounce {
|
||||
animation-name: bounce;
|
||||
animation-duration: .5s;
|
||||
}
|
||||
74
views/static/default/README.md
Normal file
74
views/static/default/README.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
|
||||
# **This box URL belongs to no one.**
|
||||
|
||||
**Drop a file** with the `Edit code` field of your choice **to claim this URL as yours**.
|
||||
|
||||
So as long as you **keep your edit code**, you will be able to make permissive moditifications to this box,
|
||||
including visualizing files, moderation and exclusion of files.
|
||||
|
||||
**To make modifications to this readme section, drop a file called `README.md`** that will override the
|
||||
currently set header. This is how you're supposed to make edits to this section.
|
||||
|
||||
New boxes have been set to have a **maximum lifetime of two years,** after the lifetime period is over,
|
||||
the box files are going to be deleted and the URL is going to be reclaimed. You'll no longer own this URL.
|
||||
|
||||
Once you've claimed this box, you'll be able to **drop a `config.json5`** with valid [`json5`](https://json5.org/) options
|
||||
**to replace the default box configuration.** [The default configuration file can be found
|
||||
here](/box-configurations#) along with all customizeble options.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Can I set a maximum upload size?
|
||||
|
||||
Works is in progress. You won't be able to set it above the maximum size set be this instance administrator
|
||||
|
||||
### Is there a views counter?
|
||||
|
||||
Works is in progress. There will be one.
|
||||
|
||||
### Can I block others from viewing the box contents?
|
||||
|
||||
Works is in progress. You will be able to.
|
||||
|
||||
### What filetypes are allowed? Can I black/white list filetypes?
|
||||
|
||||
Per the default configuration, all filetypes are allowed. but works are in progress. You will be able to
|
||||
white or blacklist filetypes.
|
||||
|
||||
### How can I see the IP addresses of the file uploaders
|
||||
|
||||
You cannot and there is no configuration you or the instance administrator can set that will allow you
|
||||
to see address of the uploaders because the servers don't collect them in the first place.
|
||||
Let it be known that the instance administrator can still set the server to run behind a reverse proxy
|
||||
which would allow him or her to collect data from the client accesses, but even still,
|
||||
this is not something the instance has access to and therefore you won't either.
|
||||
|
||||
### Is the instance adminstrator able to view the contents of this box
|
||||
|
||||
Yes they are. Whether you trust the instance adminsitrator with these files is up to you.
|
||||
|
||||
### What can't be uploaded to these boxes?
|
||||
|
||||
This is up to the instance administrator to define. See [Uploading guidelines](/uploading-guidelines#).
|
||||
|
||||
### What happens if the instance administrator changes the max lifetime of a box or the maximum upload size?
|
||||
|
||||
If the max lifetime is changed your newly created box won't be affected and your lifetime will still be the same from when the
|
||||
box was created. If the instance administrator changes te maximum upload size, your configurations will be capped to
|
||||
conform to those changes. Make sure your configurations are up to date by downloading the `config.json5` from this box at
|
||||
every periodic change.
|
||||
|
||||
### Making changes to these files is hard on a mobile UwU, why can't I edit the configurations with a form?
|
||||
|
||||
Not our problem. Get a computer.
|
||||
|
||||
### Why json5?
|
||||
|
||||
YAML sucks. TOML sucks. JSON sucks. JSON5 sucks less. [Here's what it looks like](https://github.com/chromium/chromium/blob/feb3c9f670515edf9a88f185301cbd7794ee3e52/third_party/blink/renderer/platform/runtime_enabled_features.json5).
|
||||
Lua is an option and it actually rocks but that's a bit overkill for this project.
|
||||
|
||||
### What happens if I lose my edit code?
|
||||
|
||||
It's totally up to you to not do that.
|
||||
|
||||
<script>alert("if this is executed it means we screwed up")</script>
|
||||
256
views/template.templ
Normal file
256
views/template.templ
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
package views
|
||||
|
||||
import (
|
||||
"foobar/database"
|
||||
"foobar/model"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
templ indexLayout() {
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<title>Mude meu título!</title>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<meta name="htmx-config" content='{ "responseHandling":[{"code":"422", "swap": true}, {"code":"200", "swap": true}] }' />
|
||||
<!-- tailwind -->
|
||||
<link href="/static/css/output.css" rel="stylesheet"/>
|
||||
<!-- markdown styling -->
|
||||
<link href="/static/css/github-markdown.css" rel="stylesheet"/>
|
||||
<!-- daisyUI -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css" />
|
||||
<!-- daisyUI Themes -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@5/themes.css" rel="stylesheet" type="text/css" />
|
||||
// <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<!-- HTMX -->
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<!-- Hyperscript -->
|
||||
<script src="https://unpkg.com/hyperscript.org@0.9.14"></script>
|
||||
<style>
|
||||
html, body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="flex-wrap flex content-center justify-between flex-col bg-base-100 text-sm"
|
||||
hx-boost="true" hx-push-url="true" hx-target="this">
|
||||
{ children... }
|
||||
<footer></footer>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
templ mainCenterBody() {
|
||||
@indexLayout() {
|
||||
<main class="w-full" >
|
||||
{ children... }
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
const toggleButtonInMeUntilLoad =
|
||||
`on every htmx:beforeSend
|
||||
tell <button/> in me
|
||||
toggle [@disabled='true'] until htmx:afterOnLoad
|
||||
end
|
||||
end`
|
||||
|
||||
func safeURL(s string) string {
|
||||
return fmt.Sprint(templ.SafeURL(s))
|
||||
}
|
||||
|
||||
templ Index() {
|
||||
@mainCenterBody() {
|
||||
<div class="hero-content">
|
||||
<div class="max-w-md text-center">
|
||||
<h1>hakobox</h1>
|
||||
<p>nothing here...</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
templ boxUploadSidebar(boxURL model.BoxURL) {
|
||||
@mainCenterBody() {
|
||||
<div class="">
|
||||
<section class="static md:fixed top-0 left-0 z-10 w-full md:w-3/9 p-10 bg-base-200 h-full ">
|
||||
<header>
|
||||
<h1>hakobox</h1>
|
||||
<p>It's temporary file sharing but everyone gets to contribute! </p>
|
||||
<p class="leading-tight font-medium text-black">Drop in a file to be added to this box </p>
|
||||
</header>
|
||||
<form id="add-file"
|
||||
hx-trigger="submit"
|
||||
hx-post={safeURL(fmt.Sprintf("/box/%s", boxURL))}
|
||||
hx-encoding="multipart/form-data">
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">Pick a file</legend>
|
||||
<input name="file" type="file" class="file-input" style="width: 100%;" multiple required/>
|
||||
<label class="fieldset-label">Max size 2MB</label>
|
||||
</fieldset>
|
||||
<fieldset class="fieldset">
|
||||
<legend class="fieldset-legend">Edit code</legend>
|
||||
<input type="text" class="input" name="edit_code" placeholder="Type here" style="width: 100%;"/>
|
||||
<p class="label">Optional</p>
|
||||
</fieldset>
|
||||
@errorTarget()
|
||||
<button type="submit" class="btn btn-primary mb-5">Add to the box!</button>
|
||||
<div class="text-center">
|
||||
<a class="link link-primary block mb-0"
|
||||
href={templ.SafeURL(fmt.Sprintf("/box/%s/inside", boxURL))}>
|
||||
See what's inside
|
||||
</a>
|
||||
<a class="link link-primary block mb-0"
|
||||
href={templ.SafeURL(fmt.Sprintf("/box/%s", boxURL))}>
|
||||
Front page
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<section class="md:px-20 p-4 md:my-40 m-0 md:ml-auto md:max-w-6/9">
|
||||
{ children... }
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ BoxUpload(box model.Box) {
|
||||
@boxUploadSidebar(box.Url()) {
|
||||
<div class="markdown-body">
|
||||
@templ.Raw(box.Header())
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ NewBox(boxURL model.BoxURL, templateReadme model.Markdown) {
|
||||
@boxUploadSidebar(boxURL) {
|
||||
<div class="markdown-body">
|
||||
@templ.Raw(templateReadme)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
templ Box(boxURL model.BoxURL, files []model.File) {
|
||||
@boxUploadSidebar(boxURL) {
|
||||
<main class="inline-flex flex-wrap gap-0 md:gap-5 mt-20 w-full h-full justify-start md:justify-center">
|
||||
if len(files) == 0 {
|
||||
<h2 class="m-auto">Empty box.</h2>
|
||||
} else {
|
||||
for i := range files {
|
||||
@fileCard(files[i])
|
||||
}
|
||||
}
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
func fileDisplay(file model.File) templ.Component {
|
||||
switch {
|
||||
case file.Mime().IsImage():
|
||||
return fileImageFigure(file)
|
||||
case file.Mime().IsVideo():
|
||||
return fileVideoFigure(file)
|
||||
default:
|
||||
return unknownFileFigure()
|
||||
// panic(fmt.Errorf("unrecognized mime: %s", file.Mime))
|
||||
}
|
||||
}
|
||||
|
||||
templ fileCard(file model.File) {
|
||||
<a class="card w-1/2 md:w-2xs aspect-square mb-10" href={templ.SafeURL(fmt.Sprintf("/static/box/%s", file.ID()))} target="_blank">
|
||||
<figure class="flex flex-col p-1 m-auto mb-auto md:mb-0 text-center">
|
||||
@fileDisplay(file)
|
||||
</figure>
|
||||
<div class="hidden md:inline font-light text-sm" style="text-wrap: nowrap;">
|
||||
<p title={file.Name()}>{shorten(file.Name())}</p>
|
||||
<p title={file.Name()}>{file.CreatedAt().Format(time.DateOnly)}</p>
|
||||
<p>{formatSize(file.Size())} {file.Mime().PrettyType()}</p>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
templ fileImageFigure(file model.File) {
|
||||
<img
|
||||
title={file.Name()}
|
||||
loading="lazy"
|
||||
src={fmt.Sprintf("/static/box/%s", file.ID())}
|
||||
alt={file.Name()} />
|
||||
}
|
||||
|
||||
templ fileVideoFigure(file model.File) {
|
||||
<video title={file.Name()} loading="lazy" alt={file.Name()} >
|
||||
<source src={fmt.Sprintf("/static/box/%s", file.ID())}/>
|
||||
</video>
|
||||
}
|
||||
|
||||
|
||||
templ unknownFileFigure() {
|
||||
<div>unknown format!</div>
|
||||
}
|
||||
|
||||
|
||||
func shorten(text string) string {
|
||||
if len(text) > 41 {
|
||||
return fmt.Sprintf("%s...", text[:41-3])
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
func formatSize(size int64) string {
|
||||
units := []string{"b", "Kb", "Mb", "Gb"}
|
||||
i := 0
|
||||
fSize := float64(size)
|
||||
for fSize >= 1000 && i < len(units)-1 {
|
||||
fSize /= 1000
|
||||
i++
|
||||
}
|
||||
return fmt.Sprintf("%.2f %s", fSize, units[i])
|
||||
}
|
||||
|
||||
templ FourOfour() {
|
||||
@indexLayout() {
|
||||
<div>404</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ Logged(usuario *database.Usuario) {
|
||||
@indexLayout() {
|
||||
<p>Logado como: { usuario.Nome }</p>
|
||||
<div style="text-align: center;">
|
||||
<p>
|
||||
<a href="/">index</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="/logout">logout</a>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ errorTarget() {
|
||||
<div id="error-target">
|
||||
</div>
|
||||
}
|
||||
|
||||
templ ErrorBox(msg string) {
|
||||
<div role="alert" class="bounce mt-3 relative flex flex-col w-full p-3 text-sm text-white bg-red-600 rounded-md">
|
||||
<p class="flex text-base">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="h-5 w-5 mr-2 mt-0.5"><path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"></path></svg>
|
||||
Error
|
||||
</p>
|
||||
<p class="ml-4 p-3">
|
||||
{ msg }
|
||||
</p>
|
||||
|
||||
<button class="flex items-center justify-center transition-all w-8 h-8 rounded-md text-white hover:bg-white/10 active:bg-white/10 absolute top-1.5 right-1.5" type="button"
|
||||
_='on click tell closest <div[role="alert"]/> transition opacity to 0 then remove yourself end'
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="h-5 w-5" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue