From c21f56914416130ac457692093dc31b9b331ec41 Mon Sep 17 00:00:00 2001 From: silva guimaraes Date: Mon, 21 Apr 2025 17:59:57 -0300 Subject: [PATCH] initial commit --- .air.toml | 32 + .gitignore | 6 + Dockerfile | 31 + Makefile | 26 + README.md | 6 + database/box.go | 75 + database/database.go | 86 ++ database/database_test.go | 82 ++ database/file.go | 65 + .../20250202024620_init_usuario.sql | 36 + database/migrations/20250412193502_file.sql | 36 + .../migrations/20250414041807_file_mime.sql | 32 + .../migrations/20250415031815_box_table.sql | 71 + ...20250419204935_header_message_increase.sql | 22 + .../20250421015240_box_header_to_text.sql | 20 + database/model.go | 47 + database/pgmodeler/model.dbm | 153 ++ database/usuario.go | 69 + docker-compose.yml | 73 + go.mod | 53 + go.sum | 166 +++ metrics/main.go | 46 + model/main.go | 239 ++++ model/main_test.go | 21 + prometheus/prometheus.yml | 23 + routes/auth.go | 69 + routes/box.go | 69 + routes/file.go | 204 +++ routes/logging.go | 97 ++ routes/middleware.go | 97 ++ routes/routes.go | 178 +++ server/server.go | 79 ++ tailwind.config.js | 11 + views/static/css/github-markdown.css | 1228 +++++++++++++++++ views/static/css/input.css | 78 ++ views/static/default/README.md | 74 + views/template.templ | 256 ++++ 37 files changed, 3956 insertions(+) create mode 100644 .air.toml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 database/box.go create mode 100644 database/database.go create mode 100644 database/database_test.go create mode 100644 database/file.go create mode 100644 database/migrations/20250202024620_init_usuario.sql create mode 100644 database/migrations/20250412193502_file.sql create mode 100644 database/migrations/20250414041807_file_mime.sql create mode 100644 database/migrations/20250415031815_box_table.sql create mode 100644 database/migrations/20250419204935_header_message_increase.sql create mode 100644 database/migrations/20250421015240_box_header_to_text.sql create mode 100644 database/model.go create mode 100644 database/pgmodeler/model.dbm create mode 100644 database/usuario.go create mode 100644 docker-compose.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 metrics/main.go create mode 100644 model/main.go create mode 100644 model/main_test.go create mode 100644 prometheus/prometheus.yml create mode 100644 routes/auth.go create mode 100644 routes/box.go create mode 100644 routes/file.go create mode 100644 routes/logging.go create mode 100644 routes/middleware.go create mode 100644 routes/routes.go create mode 100644 server/server.go create mode 100644 tailwind.config.js create mode 100644 views/static/css/github-markdown.css create mode 100644 views/static/css/input.css create mode 100644 views/static/default/README.md create mode 100644 views/template.templ diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..3b79808 --- /dev/null +++ b/.air.toml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd9ca31 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.env +views/static/css/output.css +*_templ.go +tmp/* +routes/tmp/* +views/static/box/* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ef356bd --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5f0405f --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..dd3e380 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ + +# **This box URL belongs to me!** + +teste + + diff --git a/database/box.go b/database/box.go new file mode 100644 index 0000000..6b6a7f3 --- /dev/null +++ b/database/box.go @@ -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 +} diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..91d245d --- /dev/null +++ b/database/database.go @@ -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 +} diff --git a/database/database_test.go b/database/database_test.go new file mode 100644 index 0000000..d492396 --- /dev/null +++ b/database/database_test.go @@ -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) + } + +} diff --git a/database/file.go b/database/file.go new file mode 100644 index 0000000..155f292 --- /dev/null +++ b/database/file.go @@ -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 +} diff --git a/database/migrations/20250202024620_init_usuario.sql b/database/migrations/20250202024620_init_usuario.sql new file mode 100644 index 0000000..f965c89 --- /dev/null +++ b/database/migrations/20250202024620_init_usuario.sql @@ -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 -- + + diff --git a/database/migrations/20250412193502_file.sql b/database/migrations/20250412193502_file.sql new file mode 100644 index 0000000..43d409c --- /dev/null +++ b/database/migrations/20250412193502_file.sql @@ -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 -- + diff --git a/database/migrations/20250414041807_file_mime.sql b/database/migrations/20250414041807_file_mime.sql new file mode 100644 index 0000000..1015ed8 --- /dev/null +++ b/database/migrations/20250414041807_file_mime.sql @@ -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 -- + + diff --git a/database/migrations/20250415031815_box_table.sql b/database/migrations/20250415031815_box_table.sql new file mode 100644 index 0000000..441b774 --- /dev/null +++ b/database/migrations/20250415031815_box_table.sql @@ -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 -- + diff --git a/database/migrations/20250419204935_header_message_increase.sql b/database/migrations/20250419204935_header_message_increase.sql new file mode 100644 index 0000000..e7f4c57 --- /dev/null +++ b/database/migrations/20250419204935_header_message_increase.sql @@ -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 -- diff --git a/database/migrations/20250421015240_box_header_to_text.sql b/database/migrations/20250421015240_box_header_to_text.sql new file mode 100644 index 0000000..0e0af2c --- /dev/null +++ b/database/migrations/20250421015240_box_header_to_text.sql @@ -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 -- diff --git a/database/model.go b/database/model.go new file mode 100644 index 0000000..d06d6a4 --- /dev/null +++ b/database/model.go @@ -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 +} diff --git a/database/pgmodeler/model.dbm b/database/pgmodeler/model.dbm new file mode 100644 index 0000000..4c9171d --- /dev/null +++ b/database/pgmodeler/model.dbm @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
diff --git a/database/usuario.go b/database/usuario.go new file mode 100644 index 0000000..864ce2d --- /dev/null +++ b/database/usuario.go @@ -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 +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7a6d2d5 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0d3a01c --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..09aed8a --- /dev/null +++ b/go.sum @@ -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= diff --git a/metrics/main.go b/metrics/main.go new file mode 100644 index 0000000..1359de5 --- /dev/null +++ b/metrics/main.go @@ -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()) +} diff --git a/model/main.go b/model/main.go new file mode 100644 index 0000000..8cdbd04 --- /dev/null +++ b/model/main.go @@ -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")) +} diff --git a/model/main_test.go b/model/main_test.go new file mode 100644 index 0000000..0859e3d --- /dev/null +++ b/model/main_test.go @@ -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)) + } +} diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml new file mode 100644 index 0000000..2223cdd --- /dev/null +++ b/prometheus/prometheus.yml @@ -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=` 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' diff --git a/routes/auth.go b/routes/auth.go new file mode 100644 index 0000000..8d75688 --- /dev/null +++ b/routes/auth.go @@ -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) +} diff --git a/routes/box.go b/routes/box.go new file mode 100644 index 0000000..cf79815 --- /dev/null +++ b/routes/box.go @@ -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))) +} diff --git a/routes/file.go b/routes/file.go new file mode 100644 index 0000000..df93135 --- /dev/null +++ b/routes/file.go @@ -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 + +} diff --git a/routes/logging.go b/routes/logging.go new file mode 100644 index 0000000..d34b65b --- /dev/null +++ b/routes/logging.go @@ -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"), + ) +} diff --git a/routes/middleware.go b/routes/middleware.go new file mode 100644 index 0000000..9a4f247 --- /dev/null +++ b/routes/middleware.go @@ -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) + } + } +} diff --git a/routes/routes.go b/routes/routes.go new file mode 100644 index 0000000..4fcda1e --- /dev/null +++ b/routes/routes.go @@ -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) +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..9cae133 --- /dev/null +++ b/server/server.go @@ -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)) +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..f4a5df6 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,11 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + // "./internal/**/*.{go,js,templ,html}", + "./views/html/*.html" + ], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/views/static/css/github-markdown.css b/views/static/css/github-markdown.css new file mode 100644 index 0000000..081b1d0 --- /dev/null +++ b/views/static/css/github-markdown.css @@ -0,0 +1,1228 @@ +.markdown-body { + --base-size-4: 0.25rem; + --base-size-8: 0.5rem; + --base-size-16: 1rem; + --base-size-24: 1.5rem; + --base-size-40: 2.5rem; + --base-text-weight-normal: 400; + --base-text-weight-medium: 500; + --base-text-weight-semibold: 600; + --fontStack-monospace: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + --fgColor-accent: Highlight; +} +@media (prefers-color-scheme: dark) { + .markdown-body, [data-theme="dark"] { + /* dark */ + color-scheme: dark; + --focus-outlineColor: #1f6feb; + --fgColor-default: #f0f6fc; + --fgColor-muted: #9198a1; + --fgColor-accent: #4493f8; + --fgColor-success: #3fb950; + --fgColor-attention: #d29922; + --fgColor-danger: #f85149; + --fgColor-done: #ab7df8; + --bgColor-default: #0d1117; + --bgColor-muted: #151b23; + --bgColor-neutral-muted: #656c7633; + --bgColor-attention-muted: #bb800926; + --borderColor-default: #3d444d; + --borderColor-muted: #3d444db3; + --borderColor-neutral-muted: #3d444db3; + --borderColor-accent-emphasis: #1f6feb; + --borderColor-success-emphasis: #238636; + --borderColor-attention-emphasis: #9e6a03; + --borderColor-danger-emphasis: #da3633; + --borderColor-done-emphasis: #8957e5; + --color-prettylights-syntax-comment: #9198a1; + --color-prettylights-syntax-constant: #79c0ff; + --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; + --color-prettylights-syntax-entity: #d2a8ff; + --color-prettylights-syntax-storage-modifier-import: #f0f6fc; + --color-prettylights-syntax-entity-tag: #7ee787; + --color-prettylights-syntax-keyword: #ff7b72; + --color-prettylights-syntax-string: #a5d6ff; + --color-prettylights-syntax-variable: #ffa657; + --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; + --color-prettylights-syntax-brackethighlighter-angle: #9198a1; + --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; + --color-prettylights-syntax-invalid-illegal-bg: #8e1519; + --color-prettylights-syntax-carriage-return-text: #f0f6fc; + --color-prettylights-syntax-carriage-return-bg: #b62324; + --color-prettylights-syntax-string-regexp: #7ee787; + --color-prettylights-syntax-markup-list: #f2cc60; + --color-prettylights-syntax-markup-heading: #1f6feb; + --color-prettylights-syntax-markup-italic: #f0f6fc; + --color-prettylights-syntax-markup-bold: #f0f6fc; + --color-prettylights-syntax-markup-deleted-text: #ffdcd7; + --color-prettylights-syntax-markup-deleted-bg: #67060c; + --color-prettylights-syntax-markup-inserted-text: #aff5b4; + --color-prettylights-syntax-markup-inserted-bg: #033a16; + --color-prettylights-syntax-markup-changed-text: #ffdfb6; + --color-prettylights-syntax-markup-changed-bg: #5a1e02; + --color-prettylights-syntax-markup-ignored-text: #f0f6fc; + --color-prettylights-syntax-markup-ignored-bg: #1158c7; + --color-prettylights-syntax-meta-diff-range: #d2a8ff; + --color-prettylights-syntax-sublimelinter-gutter-mark: #3d444d; + } +} +@media (prefers-color-scheme: light) { + .markdown-body, [data-theme="light"] { + /* light */ + color-scheme: light; + --focus-outlineColor: #0969da; + --fgColor-default: #1f2328; + --fgColor-muted: #59636e; + --fgColor-accent: #0969da; + --fgColor-success: #1a7f37; + --fgColor-attention: #9a6700; + --fgColor-danger: #d1242f; + --fgColor-done: #8250df; + --bgColor-default: #ffffff; + --bgColor-muted: #f6f8fa; + --bgColor-neutral-muted: #818b981f; + --bgColor-attention-muted: #fff8c5; + --borderColor-default: #d1d9e0; + --borderColor-muted: #d1d9e0b3; + --borderColor-neutral-muted: #d1d9e0b3; + --borderColor-accent-emphasis: #0969da; + --borderColor-success-emphasis: #1a7f37; + --borderColor-attention-emphasis: #9a6700; + --borderColor-danger-emphasis: #cf222e; + --borderColor-done-emphasis: #8250df; + --color-prettylights-syntax-comment: #59636e; + --color-prettylights-syntax-constant: #0550ae; + --color-prettylights-syntax-constant-other-reference-link: #0a3069; + --color-prettylights-syntax-entity: #6639ba; + --color-prettylights-syntax-storage-modifier-import: #1f2328; + --color-prettylights-syntax-entity-tag: #0550ae; + --color-prettylights-syntax-keyword: #cf222e; + --color-prettylights-syntax-string: #0a3069; + --color-prettylights-syntax-variable: #953800; + --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; + --color-prettylights-syntax-brackethighlighter-angle: #59636e; + --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; + --color-prettylights-syntax-invalid-illegal-bg: #82071e; + --color-prettylights-syntax-carriage-return-text: #f6f8fa; + --color-prettylights-syntax-carriage-return-bg: #cf222e; + --color-prettylights-syntax-string-regexp: #116329; + --color-prettylights-syntax-markup-list: #3b2300; + --color-prettylights-syntax-markup-heading: #0550ae; + --color-prettylights-syntax-markup-italic: #1f2328; + --color-prettylights-syntax-markup-bold: #1f2328; + --color-prettylights-syntax-markup-deleted-text: #82071e; + --color-prettylights-syntax-markup-deleted-bg: #ffebe9; + --color-prettylights-syntax-markup-inserted-text: #116329; + --color-prettylights-syntax-markup-inserted-bg: #dafbe1; + --color-prettylights-syntax-markup-changed-text: #953800; + --color-prettylights-syntax-markup-changed-bg: #ffd8b5; + --color-prettylights-syntax-markup-ignored-text: #d1d9e0; + --color-prettylights-syntax-markup-ignored-bg: #0550ae; + --color-prettylights-syntax-meta-diff-range: #8250df; + --color-prettylights-syntax-sublimelinter-gutter-mark: #818b98; + } +} + +.markdown-body { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + /* margin: 0; */ + color: var(--fgColor-default); + background-color: var(--bgColor-default); + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; +} + +.markdown-body .octicon { + display: inline-block; + fill: currentColor; + vertical-align: text-bottom; +} + +.markdown-body h1:hover .anchor .octicon-link:before, +.markdown-body h2:hover .anchor .octicon-link:before, +.markdown-body h3:hover .anchor .octicon-link:before, +.markdown-body h4:hover .anchor .octicon-link:before, +.markdown-body h5:hover .anchor .octicon-link:before, +.markdown-body h6:hover .anchor .octicon-link:before { + width: 16px; + height: 16px; + content: ' '; + display: inline-block; + background-color: currentColor; + -webkit-mask-image: url("data:image/svg+xml,"); + mask-image: url("data:image/svg+xml,"); +} + +.markdown-body details, +.markdown-body figcaption, +.markdown-body figure { + display: block; +} + +.markdown-body summary { + display: list-item; +} + +.markdown-body [hidden] { + display: none !important; +} + +.markdown-body a { + background-color: transparent; + color: var(--fgColor-accent); + text-decoration: none; +} + +.markdown-body abbr[title] { + border-bottom: none; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +.markdown-body b, +.markdown-body strong { + font-weight: var(--base-text-weight-semibold, 600); +} + +.markdown-body dfn { + font-style: italic; +} + +.markdown-body h1 { + margin: .67em 0; + font-weight: var(--base-text-weight-semibold, 600); + padding-bottom: .3em; + font-size: 2em; + border-bottom: 1px solid var(--borderColor-muted); +} + +.markdown-body mark { + background-color: var(--bgColor-attention-muted); + color: var(--fgColor-default); +} + +.markdown-body small { + font-size: 90%; +} + +.markdown-body sub, +.markdown-body sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +.markdown-body sub { + bottom: -0.25em; +} + +.markdown-body sup { + top: -0.5em; +} + +.markdown-body img { + border-style: none; + max-width: 100%; + box-sizing: content-box; +} + +.markdown-body code, +.markdown-body kbd, +.markdown-body pre, +.markdown-body samp { + font-family: monospace; + font-size: 1em; +} + +.markdown-body figure { + margin: 1em var(--base-size-40); +} + +.markdown-body hr { + box-sizing: content-box; + overflow: hidden; + background: transparent; + border-bottom: 1px solid var(--borderColor-muted); + height: .25em; + padding: 0; + margin: var(--base-size-24) 0; + background-color: var(--borderColor-default); + border: 0; +} + +.markdown-body input { + font: inherit; + margin: 0; + overflow: visible; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.markdown-body [type=button], +.markdown-body [type=reset], +.markdown-body [type=submit] { + -webkit-appearance: button; + appearance: button; +} + +.markdown-body [type=checkbox], +.markdown-body [type=radio] { + box-sizing: border-box; + padding: 0; +} + +.markdown-body [type=number]::-webkit-inner-spin-button, +.markdown-body [type=number]::-webkit-outer-spin-button { + height: auto; +} + +.markdown-body [type=search]::-webkit-search-cancel-button, +.markdown-body [type=search]::-webkit-search-decoration { + -webkit-appearance: none; + appearance: none; +} + +.markdown-body ::-webkit-input-placeholder { + color: inherit; + opacity: .54; +} + +.markdown-body ::-webkit-file-upload-button { + -webkit-appearance: button; + appearance: button; + font: inherit; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body ::placeholder { + color: var(--fgColor-muted); + opacity: 1; +} + +.markdown-body hr::before { + display: table; + content: ""; +} + +.markdown-body hr::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body table { + border-spacing: 0; + border-collapse: collapse; + display: block; + width: max-content; + max-width: 100%; + overflow: auto; + font-variant: tabular-nums; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body details summary { + cursor: pointer; +} + +.markdown-body a:focus, +.markdown-body [role=button]:focus, +.markdown-body input[type=radio]:focus, +.markdown-body input[type=checkbox]:focus { + outline: 2px solid var(--focus-outlineColor); + outline-offset: -2px; + box-shadow: none; +} + +.markdown-body a:focus:not(:focus-visible), +.markdown-body [role=button]:focus:not(:focus-visible), +.markdown-body input[type=radio]:focus:not(:focus-visible), +.markdown-body input[type=checkbox]:focus:not(:focus-visible) { + outline: solid 1px transparent; +} + +.markdown-body a:focus-visible, +.markdown-body [role=button]:focus-visible, +.markdown-body input[type=radio]:focus-visible, +.markdown-body input[type=checkbox]:focus-visible { + outline: 2px solid var(--focus-outlineColor); + outline-offset: -2px; + box-shadow: none; +} + +.markdown-body a:not([class]):focus, +.markdown-body a:not([class]):focus-visible, +.markdown-body input[type=radio]:focus, +.markdown-body input[type=radio]:focus-visible, +.markdown-body input[type=checkbox]:focus, +.markdown-body input[type=checkbox]:focus-visible { + outline-offset: 0; +} + +.markdown-body kbd { + display: inline-block; + padding: var(--base-size-4); + font: 11px var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace); + line-height: 10px; + color: var(--fgColor-default); + vertical-align: middle; + background-color: var(--bgColor-muted); + border: solid 1px var(--borderColor-neutral-muted); + border-bottom-color: var(--borderColor-neutral-muted); + border-radius: 6px; + box-shadow: inset 0 -1px 0 var(--borderColor-neutral-muted); +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: var(--base-size-24); + margin-bottom: var(--base-size-16); + font-weight: var(--base-text-weight-semibold, 600); + line-height: 1.25; +} + +.markdown-body h2 { + font-weight: var(--base-text-weight-semibold, 600); + padding-bottom: .3em; + font-size: 1.5em; + border-bottom: 1px solid var(--borderColor-muted); +} + +.markdown-body h3 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: 1.25em; +} + +.markdown-body h4 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: 1em; +} + +.markdown-body h5 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: .875em; +} + +.markdown-body h6 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: .85em; + color: var(--fgColor-muted); +} + +.markdown-body p { + margin-top: 0; + margin-bottom: 10px; +} + +.markdown-body blockquote { + margin: 0; + padding: 0 1em; + color: var(--fgColor-muted); + border-left: .25em solid var(--borderColor-default); +} + +.markdown-body ul, +.markdown-body ol { + margin-top: 0; + margin-bottom: 0; + padding-left: 2em; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ul ul ol, +.markdown-body ul ol ol, +.markdown-body ol ul ol, +.markdown-body ol ol ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body tt, +.markdown-body code, +.markdown-body samp { + font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace); + font-size: 12px; +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; + font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace); + font-size: 12px; + word-wrap: normal; +} + +.markdown-body .octicon { + display: inline-block; + overflow: visible !important; + vertical-align: text-bottom; + fill: currentColor; +} + +.markdown-body input::-webkit-outer-spin-button, +.markdown-body input::-webkit-inner-spin-button { + margin: 0; + appearance: none; +} + +.markdown-body .mr-2 { + margin-right: var(--base-size-8, 8px) !important; +} + +.markdown-body::before { + display: table; + content: ""; +} + +.markdown-body::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body>*:first-child { + margin-top: 0 !important; +} + +.markdown-body>*:last-child { + margin-bottom: 0 !important; +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none; +} + +.markdown-body .absent { + color: var(--fgColor-danger); +} + +.markdown-body .anchor { + float: left; + padding-right: var(--base-size-4); + margin-left: -20px; + line-height: 1; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre, +.markdown-body details { + margin-top: 0; + margin-bottom: var(--base-size-16); +} + +.markdown-body blockquote>:first-child { + margin-top: 0; +} + +.markdown-body blockquote>:last-child { + margin-bottom: 0; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: var(--fgColor-default); + vertical-align: middle; + visibility: hidden; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible; +} + +.markdown-body h1 tt, +.markdown-body h1 code, +.markdown-body h2 tt, +.markdown-body h2 code, +.markdown-body h3 tt, +.markdown-body h3 code, +.markdown-body h4 tt, +.markdown-body h4 code, +.markdown-body h5 tt, +.markdown-body h5 code, +.markdown-body h6 tt, +.markdown-body h6 code { + padding: 0 .2em; + font-size: inherit; +} + +.markdown-body summary h1, +.markdown-body summary h2, +.markdown-body summary h3, +.markdown-body summary h4, +.markdown-body summary h5, +.markdown-body summary h6 { + display: inline-block; +} + +.markdown-body summary h1 .anchor, +.markdown-body summary h2 .anchor, +.markdown-body summary h3 .anchor, +.markdown-body summary h4 .anchor, +.markdown-body summary h5 .anchor, +.markdown-body summary h6 .anchor { + margin-left: -40px; +} + +.markdown-body summary h1, +.markdown-body summary h2 { + padding-bottom: 0; + border-bottom: 0; +} + +.markdown-body ul.no-list, +.markdown-body ol.no-list { + padding: 0; + list-style-type: none; +} + +.markdown-body ol[type="a s"] { + list-style-type: lower-alpha; +} + +.markdown-body ol[type="A s"] { + list-style-type: upper-alpha; +} + +.markdown-body ol[type="i s"] { + list-style-type: lower-roman; +} + +.markdown-body ol[type="I s"] { + list-style-type: upper-roman; +} + +.markdown-body ol[type="1"] { + list-style-type: decimal; +} + +.markdown-body div>ol:not([type]) { + list-style-type: decimal; +} + +.markdown-body ul ul, +.markdown-body ul ol, +.markdown-body ol ol, +.markdown-body ol ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li>p { + margin-top: var(--base-size-16); +} + +.markdown-body li+li { + margin-top: .25em; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: var(--base-size-16); + font-size: 1em; + font-style: italic; + font-weight: var(--base-text-weight-semibold, 600); +} + +.markdown-body dl dd { + padding: 0 var(--base-size-16); + margin-bottom: var(--base-size-16); +} + +.markdown-body table th { + font-weight: var(--base-text-weight-semibold, 600); +} + +.markdown-body table th, +.markdown-body table td { + padding: 6px 13px; + border: 1px solid var(--borderColor-default); +} + +.markdown-body table td>:last-child { + margin-bottom: 0; +} + +.markdown-body table tr { + background-color: var(--bgColor-default); + border-top: 1px solid var(--borderColor-muted); +} + +.markdown-body table tr:nth-child(2n) { + background-color: var(--bgColor-muted); +} + +.markdown-body table img { + background-color: transparent; +} + +.markdown-body img[align=right] { + padding-left: 20px; +} + +.markdown-body img[align=left] { + padding-right: 20px; +} + +.markdown-body .emoji { + max-width: none; + vertical-align: text-top; + background-color: transparent; +} + +.markdown-body span.frame { + display: block; + overflow: hidden; +} + +.markdown-body span.frame>span { + display: block; + float: left; + width: auto; + padding: 7px; + margin: 13px 0 0; + overflow: hidden; + border: 1px solid var(--borderColor-default); +} + +.markdown-body span.frame span img { + display: block; + float: left; +} + +.markdown-body span.frame span span { + display: block; + padding: 5px 0 0; + clear: both; + color: var(--fgColor-default); +} + +.markdown-body span.align-center { + display: block; + overflow: hidden; + clear: both; +} + +.markdown-body span.align-center>span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: center; +} + +.markdown-body span.align-center span img { + margin: 0 auto; + text-align: center; +} + +.markdown-body span.align-right { + display: block; + overflow: hidden; + clear: both; +} + +.markdown-body span.align-right>span { + display: block; + margin: 13px 0 0; + overflow: hidden; + text-align: right; +} + +.markdown-body span.align-right span img { + margin: 0; + text-align: right; +} + +.markdown-body span.float-left { + display: block; + float: left; + margin-right: 13px; + overflow: hidden; +} + +.markdown-body span.float-left span { + margin: 13px 0 0; +} + +.markdown-body span.float-right { + display: block; + float: right; + margin-left: 13px; + overflow: hidden; +} + +.markdown-body span.float-right>span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: right; +} + +.markdown-body code, +.markdown-body tt { + padding: .2em .4em; + margin: 0; + font-size: 85%; + white-space: break-spaces; + background-color: var(--bgColor-neutral-muted); + border-radius: 6px; +} + +.markdown-body code br, +.markdown-body tt br { + display: none; +} + +.markdown-body del code { + text-decoration: inherit; +} + +.markdown-body samp { + font-size: 85%; +} + +.markdown-body pre code { + font-size: 100%; +} + +.markdown-body pre>code { + padding: 0; + margin: 0; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body .highlight { + margin-bottom: var(--base-size-16); +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: var(--base-size-16); + overflow: auto; + font-size: 85%; + line-height: 1.45; + color: var(--fgColor-default); + background-color: var(--bgColor-muted); + border-radius: 6px; +} + +.markdown-body pre code, +.markdown-body pre tt { + display: inline; + max-width: auto; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: transparent; + border: 0; +} + +.markdown-body .csv-data td, +.markdown-body .csv-data th { + padding: 5px; + overflow: hidden; + font-size: 12px; + line-height: 1; + text-align: left; + white-space: nowrap; +} + +.markdown-body .csv-data .blob-num { + padding: 10px var(--base-size-8) 9px; + text-align: right; + background: var(--bgColor-default); + border: 0; +} + +.markdown-body .csv-data tr { + border-top: 0; +} + +.markdown-body .csv-data th { + font-weight: var(--base-text-weight-semibold, 600); + background: var(--bgColor-muted); + border-top: 0; +} + +.markdown-body [data-footnote-ref]::before { + content: "["; +} + +.markdown-body [data-footnote-ref]::after { + content: "]"; +} + +.markdown-body .footnotes { + font-size: 12px; + color: var(--fgColor-muted); + border-top: 1px solid var(--borderColor-default); +} + +.markdown-body .footnotes ol { + padding-left: var(--base-size-16); +} + +.markdown-body .footnotes ol ul { + display: inline-block; + padding-left: var(--base-size-16); + margin-top: var(--base-size-16); +} + +.markdown-body .footnotes li { + position: relative; +} + +.markdown-body .footnotes li:target::before { + position: absolute; + top: calc(var(--base-size-8)*-1); + right: calc(var(--base-size-8)*-1); + bottom: calc(var(--base-size-8)*-1); + left: calc(var(--base-size-24)*-1); + pointer-events: none; + content: ""; + border: 2px solid var(--borderColor-accent-emphasis); + border-radius: 6px; +} + +.markdown-body .footnotes li:target { + color: var(--fgColor-default); +} + +.markdown-body .footnotes .data-footnote-backref g-emoji { + font-family: monospace; +} + +.markdown-body body:has(:modal) { + padding-right: var(--dialog-scrollgutter) !important; +} + +.markdown-body .pl-c { + color: var(--color-prettylights-syntax-comment); +} + +.markdown-body .pl-c1, +.markdown-body .pl-s .pl-v { + color: var(--color-prettylights-syntax-constant); +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: var(--color-prettylights-syntax-entity); +} + +.markdown-body .pl-smi, +.markdown-body .pl-s .pl-s1 { + color: var(--color-prettylights-syntax-storage-modifier-import); +} + +.markdown-body .pl-ent { + color: var(--color-prettylights-syntax-entity-tag); +} + +.markdown-body .pl-k { + color: var(--color-prettylights-syntax-keyword); +} + +.markdown-body .pl-s, +.markdown-body .pl-pds, +.markdown-body .pl-s .pl-pse .pl-s1, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sre, +.markdown-body .pl-sr .pl-sra { + color: var(--color-prettylights-syntax-string); +} + +.markdown-body .pl-v, +.markdown-body .pl-smw { + color: var(--color-prettylights-syntax-variable); +} + +.markdown-body .pl-bu { + color: var(--color-prettylights-syntax-brackethighlighter-unmatched); +} + +.markdown-body .pl-ii { + color: var(--color-prettylights-syntax-invalid-illegal-text); + background-color: var(--color-prettylights-syntax-invalid-illegal-bg); +} + +.markdown-body .pl-c2 { + color: var(--color-prettylights-syntax-carriage-return-text); + background-color: var(--color-prettylights-syntax-carriage-return-bg); +} + +.markdown-body .pl-sr .pl-cce { + font-weight: bold; + color: var(--color-prettylights-syntax-string-regexp); +} + +.markdown-body .pl-ml { + color: var(--color-prettylights-syntax-markup-list); +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + font-weight: bold; + color: var(--color-prettylights-syntax-markup-heading); +} + +.markdown-body .pl-mi { + font-style: italic; + color: var(--color-prettylights-syntax-markup-italic); +} + +.markdown-body .pl-mb { + font-weight: bold; + color: var(--color-prettylights-syntax-markup-bold); +} + +.markdown-body .pl-md { + color: var(--color-prettylights-syntax-markup-deleted-text); + background-color: var(--color-prettylights-syntax-markup-deleted-bg); +} + +.markdown-body .pl-mi1 { + color: var(--color-prettylights-syntax-markup-inserted-text); + background-color: var(--color-prettylights-syntax-markup-inserted-bg); +} + +.markdown-body .pl-mc { + color: var(--color-prettylights-syntax-markup-changed-text); + background-color: var(--color-prettylights-syntax-markup-changed-bg); +} + +.markdown-body .pl-mi2 { + color: var(--color-prettylights-syntax-markup-ignored-text); + background-color: var(--color-prettylights-syntax-markup-ignored-bg); +} + +.markdown-body .pl-mdr { + font-weight: bold; + color: var(--color-prettylights-syntax-meta-diff-range); +} + +.markdown-body .pl-ba { + color: var(--color-prettylights-syntax-brackethighlighter-angle); +} + +.markdown-body .pl-sg { + color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); +} + +.markdown-body .pl-corl { + text-decoration: underline; + color: var(--color-prettylights-syntax-constant-other-reference-link); +} + +.markdown-body [role=button]:focus:not(:focus-visible), +.markdown-body [role=tabpanel][tabindex="0"]:focus:not(:focus-visible), +.markdown-body button:focus:not(:focus-visible), +.markdown-body summary:focus:not(:focus-visible), +.markdown-body a:focus:not(:focus-visible) { + outline: none; + box-shadow: none; +} + +.markdown-body [tabindex="0"]:focus:not(:focus-visible), +.markdown-body details-dialog:focus:not(:focus-visible) { + outline: none; +} + +.markdown-body g-emoji { + display: inline-block; + min-width: 1ch; + font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; + font-size: 1em; + font-style: normal !important; + font-weight: var(--base-text-weight-normal, 400); + line-height: 1; + vertical-align: -0.075em; +} + +.markdown-body g-emoji img { + width: 1em; + height: 1em; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item label { + font-weight: var(--base-text-weight-normal, 400); +} + +.markdown-body .task-list-item.enabled label { + cursor: pointer; +} + +.markdown-body .task-list-item+.task-list-item { + margin-top: var(--base-size-4); +} + +.markdown-body .task-list-item .handle { + display: none; +} + +.markdown-body .task-list-item-checkbox { + margin: 0 .2em .25em -1.4em; + vertical-align: middle; +} + +.markdown-body ul:dir(rtl) .task-list-item-checkbox { + margin: 0 -1.6em .25em .2em; +} + +.markdown-body ol:dir(rtl) .task-list-item-checkbox { + margin: 0 -1.6em .25em .2em; +} + +.markdown-body .contains-task-list:hover .task-list-item-convert-container, +.markdown-body .contains-task-list:focus-within .task-list-item-convert-container { + display: block; + width: auto; + height: 24px; + overflow: visible; + clip: auto; +} + +.markdown-body ::-webkit-calendar-picker-indicator { + filter: invert(50%); +} + +.markdown-body .markdown-alert { + padding: var(--base-size-8) var(--base-size-16); + margin-bottom: var(--base-size-16); + color: inherit; + border-left: .25em solid var(--borderColor-default); +} + +.markdown-body .markdown-alert>:first-child { + margin-top: 0; +} + +.markdown-body .markdown-alert>:last-child { + margin-bottom: 0; +} + +.markdown-body .markdown-alert .markdown-alert-title { + display: flex; + font-weight: var(--base-text-weight-medium, 500); + align-items: center; + line-height: 1; +} + +.markdown-body .markdown-alert.markdown-alert-note { + border-left-color: var(--borderColor-accent-emphasis); +} + +.markdown-body .markdown-alert.markdown-alert-note .markdown-alert-title { + color: var(--fgColor-accent); +} + +.markdown-body .markdown-alert.markdown-alert-important { + border-left-color: var(--borderColor-done-emphasis); +} + +.markdown-body .markdown-alert.markdown-alert-important .markdown-alert-title { + color: var(--fgColor-done); +} + +.markdown-body .markdown-alert.markdown-alert-warning { + border-left-color: var(--borderColor-attention-emphasis); +} + +.markdown-body .markdown-alert.markdown-alert-warning .markdown-alert-title { + color: var(--fgColor-attention); +} + +.markdown-body .markdown-alert.markdown-alert-tip { + border-left-color: var(--borderColor-success-emphasis); +} + +.markdown-body .markdown-alert.markdown-alert-tip .markdown-alert-title { + color: var(--fgColor-success); +} + +.markdown-body .markdown-alert.markdown-alert-caution { + border-left-color: var(--borderColor-danger-emphasis); +} + +.markdown-body .markdown-alert.markdown-alert-caution .markdown-alert-title { + color: var(--fgColor-danger); +} + +.markdown-body>*:first-child>.heading-element:first-child { + margin-top: 0 !important; +} + +.markdown-body .highlight pre:has(+.zeroclipboard-container) { + min-height: 52px; +} + diff --git a/views/static/css/input.css b/views/static/css/input.css new file mode 100644 index 0000000..829c5e4 --- /dev/null +++ b/views/static/css/input.css @@ -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; +} diff --git a/views/static/default/README.md b/views/static/default/README.md new file mode 100644 index 0000000..4529890 --- /dev/null +++ b/views/static/default/README.md @@ -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. + + diff --git a/views/template.templ b/views/template.templ new file mode 100644 index 0000000..794b569 --- /dev/null +++ b/views/template.templ @@ -0,0 +1,256 @@ +package views + +import ( + "foobar/database" + "foobar/model" + "fmt" + "time" +) + +templ indexLayout() { + + + + Mude meu título! + + + + + + + + + + + + // + + + + + + + + { children... } +
+ + +} + +templ mainCenterBody() { + @indexLayout() { +
+ { children... } +
+ } +} + +const toggleButtonInMeUntilLoad = + `on every htmx:beforeSend + tell + + + +
+ { children... } +
+ + } +} + +templ BoxUpload(box model.Box) { + @boxUploadSidebar(box.Url()) { +
+ @templ.Raw(box.Header()) +
+ } +} + +templ NewBox(boxURL model.BoxURL, templateReadme model.Markdown) { + @boxUploadSidebar(boxURL) { +
+ @templ.Raw(templateReadme) +
+ } +} + + +templ Box(boxURL model.BoxURL, files []model.File) { + @boxUploadSidebar(boxURL) { +
+ if len(files) == 0 { +

Empty box.

+ } else { + for i := range files { + @fileCard(files[i]) + } + } +
+ } +} + +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) { + +
+ @fileDisplay(file) +
+ +
+} + +templ fileImageFigure(file model.File) { + {file.Name()} +} + +templ fileVideoFigure(file model.File) { + +} + + +templ unknownFileFigure() { +
unknown format!
+} + + +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() { +
404
+ } +} + +templ Logged(usuario *database.Usuario) { + @indexLayout() { +

Logado como: { usuario.Nome }

+
+

+ index +

+

+ logout +

+
+ } +} + +templ errorTarget() { +
+
+} + +templ ErrorBox(msg string) { + +}