chore: initial commit

This commit is contained in:
Mark Bailey 2024-11-11 13:48:04 -05:00
commit 133944922c
87 changed files with 6111 additions and 0 deletions

52
.air.toml Normal file
View File

@ -0,0 +1,52 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ./cmd/"
delay = 1000
exclude_dir = ["public", "data", "tmp"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
silent = false
time = false
[misc]
clean_on_exit = true
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
node_modules/**
dist/**/*
ptpp
debug
log/*
database/backup/*
project-files/.obsidian/**
**/output.css
tmp/*
data/*
data/database.db
.idea

39
Dockerfile Normal file
View File

@ -0,0 +1,39 @@
ARG GO_VERSION=1.23.2
################################################################################
# Stage: builder ###############################################################
################################################################################
FROM golang:${GO_VERSION}-alpine AS builder
WORKDIR /usr/src/app
RUN apk add npm
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY . .
RUN npm i -g tailwindcss
RUN npm i -D @tailwindcss/forms
RUN tailwindcss -i ./public/assets/css/index.css -o ./public/assets/css/output.css
RUN rm ./public/assets/css/index.css
RUN go build -v -o /ptpp-build ./cmd
################################################################################
# Stage: runner ################################################################
################################################################################
FROM alpine:latest AS runner
WORKDIR /app
ENV PATH="/root/go/bin:/app:${PATH}"
RUN apk add go
COPY --from=builder /ptpp-build .
COPY --from=builder /usr/src/app/public ./public
COPY --from=builder /usr/src/app/migrations ./migrations
CMD ["ptpp-build"]

116
README.md Normal file
View File

@ -0,0 +1,116 @@
# Golang Site Template
## What this project isn't
- A library
- A framework
## What is this project?
- A template, a starting point. An inspiration, maybe?
This project is a demo, of sorts, really. It is the base for my next side project,
which you can find at [markbailey.dev](https://markbailey.dev). This project is a
culmination of many, many hours poring over Go documentation, others' Go code,
and questions I wanted answers to, left over from previous experimentation with a
popular web framework for Go. This project is a full stack application.
This is a template for a full stack web application written in Golang.
It is fully-featured, and includes a number of tools to bootstrap a project on top
of what is already here. This project is a work in progress, it will be updated
and maintained for the foreseeable future. This is a labor of love, learning,
and a desire to build software that intrigues me outside of my day-to-day work.
I chose Go for this project because I wanted to explore the language, and the concepts
it lends itself well to. You will notice that this project contains a number of
features and patterns found in large scale projects. This is intentional, as in
my day-to-day work I write fullstack PHP in the Symfony framework. I wanted to
mirror the patterns I use at work in this project, and it has proven to be a
huge boost in my understanding of both Go and the patterns, tools, and paradigms
I use at work. Much of this project has been influenced by the advice of my
incredible mentor, friend, and boss Andy, and my guru level senior developer.
I have accumulated here the best practices that I have learned from them, from
my own experience, and from the tenacious push in the Go community to write
clean, idiomatic, and readable code.
It contains these features and more:
- Middleware (Fully customizable, custom)
- Routing and sub-routing
- In-memory sessions
- Templating
- Database interaction
This one is special, because the project uses an SQLite3 database for
local development, and a PostgreSQL database for production.
- Password authentication
- Project management via the `bin/app` script
This site is currently powering my personal site, which is self hosted on a VPS.
One of my favorite features of this project is the low number of dependencies. Check
out the following excerpt from `go.mod` (As of 2024-11-11).
```go
require (
github.com/a-h/templ v0.2.793
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.24
golang.org/x/crypto v0.27.0
)
```
There are only seven dependencies in this project, two of them being
just database drivers. There are also the "hidden" dependencies of sqlc, and TailwindCSS.
I like to think there are actually only five, if we don't count the obligatory
database drivers.
## Project Statistics
![project statistics](./project-files/11092024-ptpp-stats.png)
## Setup
1. Install Go
- Arch: `sudo pacman -S go`
- Other OS: from your package manager or visit [https://go.dev/doc/install](https://go.dev/doc/install)
2. Select a useable IDE:
- GoLand (JetBrains)
- Neovim
- Install `gopls` LSP using Mason and enable it in your `lspconfig`
- Set up formatting with `gofumpt` and `goimports`
- VSCode
- I don't use it, and I wouldn't recommend it. But you do you, boo :)
3. Clone project: `git clone https://git.markbailey.dev/cerbervs/ptpp.git`
4. Run: `make first-install`
5. Access the development site at
[http://localhost:8080](http://localhost:8080) or access the live site at
## Project Management Through `bin/app`
`bin/app` is a script that manages the project from top to bottom.
Please consult the help menu for more information: `bin/app help`
When starting from a fresh clone of the project, run
`bin/app first-install` to set up the project. This will install several
binaries, and the required node packages. You may be required to enter your
password.
## Git Conventions
### Making Branches
Branches should be prefixed with one of the following
- `feat/`
- `fix/`
- `refactor/`
- `test/`
Followed by a short description of the proposed changeset `separated-by-hyphens`
### Committing Code
This project uses
[Conventional Commits (Angular)](https://www.conventionalcommits.org/en/v1.0.0/#specification),
and in the future,
[Commitlint](https://github.com/conventional-changelog/commitlint)

394
bin/app Executable file
View File

@ -0,0 +1,394 @@
#!/bin/sh
GREEN='\033[0;32m'
ORANGE='\033[0;33m'
NO_COLOR='\033[0m'
BRIGHT_CYAN='\033[0;96m'
BRIGHT_PURPLE='\033[0;95m'
__check_go() {
if ! command -v go &>/dev/null; then
echo -e "\nGo is not installed. Please install Go."
echo -e "\nhttps://golang.org/doc/install"
_cmd_missing=true
_cmd_missing_list="${_cmd_missing_list}go "
fi
}
__check_node() {
if ! command -v npm &>/dev/null; then
echo -e "\nNode is not installed. Please install Node."
echo -e "\nhttps://nodejs.org/en/download/"
_cmd_missing=true
_cmd_missing_list="${_cmd_missing_list}npm "
fi
}
__check_dlv() {
if ! command -v dlv &>/dev/null; then
echo -e "\nDLV is not installed. Please install DLV."
echo -e "\nbin/app install-tools"
_cmd_missing=true
_cmd_missing_list="${_cmd_missing_list}dlv "
fi
}
__check_sqlc() {
if ! command -v sqlc &>/dev/null; then
echo -e "\nSQLC is not installed. Please install SQLC."
echo -e "\nbin/app install-tools"
_cmd_missing=true
_cmd_missing_list="${_cmd_missing_list}sqlc "
fi
}
__check_goose() {
if ! command -v goose &>/dev/null; then
echo -e "\nGoose is not installed. Please install Goose."
echo -e "\nbin/app install-tools"
_cmd_missing=true
_cmd_missing_list="${_cmd_missing_list}goose "
fi
}
__check_air() {
if ! command -v air &>/dev/null; then
echo -e "\nAir is not installed. Please install Air."
echo -e "\nbin/app install-tools"
_cmd_missing=true
_cmd_missing_list="${_cmd_missing_list}air "
fi
}
__check_templ() {
if ! command -v templ &>/dev/null; then
echo -e "\nTempl is not installed. Please install Templ."
echo -e "\nbin/app install-tools"
_cmd_missing=true
_cmd_missing_list="${_cmd_missing_list}templ "
fi
}
__check_tailwind() {
if ! command -v tailwindcss &>/dev/null; then
echo -e "\nTailwind is not installed. Please install Tailwind."
echo -e "\nnpm i -g tailwindcss && npm i -D @tailwindcss/forms"
_cmd_missing=true
_cmd_missing_list="${_cmd_missing_list}tailwindcss "
fi
}
__check_missing() {
export _cmd_missing=false
export _cmd_missing_list=
__check_go
__check_node
__check_dlv
__check_sqlc
__check_goose
__check_air
__check_templ
__check_tailwind
if $_cmd_missing; then
echo -e "\nMissing commands: ${_cmd_missing_list}"
export _cmd_missing=false
__cmd_missing_list=
exit 1
fi
}
__install_deps() {
echo ================================================================================
echo = Install dependencies =========================================================
echo ================================================================================
go mod download && go mod verify
echo -e "\n"
}
INSTALL_DEPS_match=install-deps
__install_project_tools() {
export _cmd_missing=false
__check_go
if $_cmd_missing; then
exit 1
fi
go install github.com/a-h/templ/cmd/templ@latest
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
go install github.com/pressly/goose/v3/cmd/goose@latest
go install github.com/air-verse/air@latest
go install github.com/go-delve/delve/cmd/dlv@latest
}
INSTALL_PROJECT_TOOLS_match=install-tools
__clean() {
echo ================================================================================
echo = Cleaning environment =========================================================
echo ================================================================================
echo -e "\n"
find . -name '*_templ.go' -delete
rm -rf ./ptpp ./debug ./public/assets/css/output.css ./dist/* ./models/*
echo -e "\n"
}
CLEAN_match=clean
__init_project() {
echo ================================================================================
echo = First install ================================================================
echo ================================================================================
mkdir -p data && mkdir -p log
if ! [ -f "data/database.db" ]; then
touch data/database.db
fi
_cmd_missing=false
__check_go
if $_cmd_missing; then
exit 1
fi
__install_project_tools
sudo -S npm install
echo -e "\n"
}
INIT_PROJECT_match=init-project
__first_install_macro() {
_and_run=${1:-}
if ! [ -z ${1+x} ]; then
shift
fi
__init_project
__install_deps
__check_missing
__clean
__generate_macro
__migrate --all
case "$_and_run" in
--and-run) __run "$@" ;;
*) ;;
esac
}
FIRST_INSTALL_MACRO_match=first-install
__build() {
_cmd_missing=false
__check_go
if $_cmd_missing; then
exit 1
fi
echo ================================================================================
echo = Building =====================================================================
echo ================================================================================
go build -v -o ptpp ./cmd
echo -e "\n"
}
BUILD_match=build
__run() {
_case=${1:-}
case "$1" in
--reload) _reload=true ;;
"") _reload=false ;;
*)
echo -e "Invalid flag $_case\n"
__help
;;
esac
__build
if $_reload; then
_cmd_missing=false
__check_air
if $_cmd_missing; then
exit 1
fi
air
echo -e "\n"
else
echo ================================================================================
echo = Running ======================================================================
echo ================================================================================
echo -e "\n"
./ptpp
fi
}
RUN_match=run
__build_debug() {
_cmd_missing=false
__check_go
__check_dlv
if $_cmd_missing; then
exit 1
fi
echo ================================================================================
echo = Building debug ==============================================================
echo ================================================================================
go build -gcflags "all=-N -l" -o debug ./cmd/main.go
echo -e "\n"
}
BUILD_DEBUG_match=build-debug
__run_debug() {
_cmd_missing=false
__check_dlv
if $_cmd_missing; then
exit 1
fi
__build_debug
echo ================================================================================
echo = Running debug ===============================================================
echo ================================================================================
dlv -l 127.0.0.1:2345 --headless --api-version=2 --accept-multiclient exec ./debug
echo -e "\n"
}
RUN_DEBUG_match=run-debug
__migrate() {
_cmd_missing=false
__check_goose
if $_cmd_missing; then
exit 1
fi
_env=${1:-dev}
if ! [ -z ${1+x} ]; then
shift
fi
_direction=${1:-up}
case "$_env" in
--all)
GOOSE_DRIVER=postgres GOOSE_MIGRATION_DIR="./migrations/postgres" goose $_direction
GOOSE_DRIVER=sqlite GOOSE_MIGRATION_DIR="./migrations/sqlite" goose $_direction
;;
dev)
GOOSE_DRIVER=sqlite GOOSE_MIGRATION_DIR="./migrations/sqlite" goose $_direction
;;
prod)
GOOSE_DRIVER=postgres GOOSE_MIGRATION_DIR="./migrations/postgres" goose $_direction
;;
esac
echo ================================================================================
echo = Migrating ====================================================================
echo ================================================================================
echo -e "\n"
}
MIGRATE_match=migrate
__sqlc() {
_cmd_missing=false
__check_sqlc
if $_cmd_missing; then
exit 1
fi
echo ================================================================================
echo = Generating SQLC ==============================================================
echo ================================================================================
sqlc generate
echo -e "\n"
}
__templ() {
echo ================================================================================
echo = Generating templates =========================================================
echo ================================================================================
templ generate
echo -e "\n"
}
TEMPL_match=templ
__tailwind() {
_cmd_missing=false
__check_tailwind
if $_cmd_missing; then
exit 1
fi
echo ================================================================================
echo = Generating Tailwind ==========================================================
echo ================================================================================
tailwindcss build -i ./public/assets/css/index.css -o ./public/assets/css/output.css
echo -e "\n"
}
TAILWIND_match=tailwind
__generate_macro() {
_base=${1:---all}
case "$_base" in
--all)
__templ
__sqlc
__tailwind
;;
--templ) __templ ;;
--sqlc) __sqlc ;;
--tailwind) __tailwind ;;
*)
echo -e "Invalid flag $_base\n"
__help
;;
esac
}
GENERATE_MACRO_match=generate
__help() {
echo -e "Usage: $0 <command>"
echo -e "Commands:"
echo -e " ${GREEN}${FIRST_INSTALL_MACRO_match}${NO_COLOR}: ${ORANGE}First install macro${NO_COLOR}"
echo -e " ${BRIGHT_PURPLE}? ${GREEN}--and-run${NO_COLOR}: ${ORANGE}Run project after first install${NO_COLOR}"
echo -e " ${BRIGHT_PURPLE}? ${GREEN}--reload${NO_COLOR}: ${ORANGE}Run project with reload${NO_COLOR}"
echo -e " ${GREEN}${CLEAN_match}${NO_COLOR}: ${ORANGE}Clean environment${NO_COLOR}"
echo -e " ${GREEN}${INSTALL_DEPS_match}${NO_COLOR}: ${ORANGE}Install dependencies${NO_COLOR}"
echo -e " ${GREEN}${INSTALL_PROJECT_TOOLS_match}${NO_COLOR}: ${ORANGE}Install project tools${NO_COLOR}"
echo -e " ${GREEN}${BUILD_match}${NO_COLOR}: ${ORANGE}Build project${NO_COLOR}"
echo -e " ${GREEN}${RUN_match}${NO_COLOR}: ${ORANGE}Run project${NO_COLOR}"
echo -e " ${BRIGHT_PURPLE}? ${GREEN}--reload${NO_COLOR}: ${ORANGE}Run project with reload${NO_COLOR}"
echo -e " ${GREEN}${GENERATE_MACRO_match}${NO_COLOR}: ${ORANGE}Run code generation${NO_COLOR}"
echo -e " ${BRIGHT_PURPLE}* ${GREEN}--all${NO_COLOR}: ${ORANGE}Generate all${NO_COLOR}"
echo -e " ${GREEN}--tailwind${NO_COLOR}: ${ORANGE}Generate Tailwind CSS${NO_COLOR}"
echo -e " ${GREEN}--templ${NO_COLOR}: ${ORANGE}Generate templates${NO_COLOR}"
echo -e " ${GREEN}--sqlc${NO_COLOR}: ${ORANGE}Generate ORM${NO_COLOR}"
echo -e " ${GREEN}${MIGRATE_match}${NO_COLOR}: ${ORANGE}Migrate database${NO_COLOR}"
echo -e " ${GREEN}--all${NO_COLOR}: ${ORANGE}Migrate all databases${NO_COLOR}"
echo -e " ${BRIGHT_PURPLE}* ${GREEN}dev${NO_COLOR}: ${ORANGE}Migrate development database${NO_COLOR}"
echo -e " ${GREEN}prod${NO_COLOR}: ${ORANGE}Migrate production database${NO_COLOR}"
echo -e " ${BRIGHT_PURPLE}?* ${GREEN}up${NO_COLOR}: ${ORANGE}Migrate up${NO_COLOR}"
echo -e " ${BRIGHT_PURPLE}? ${GREEN}down${NO_COLOR}: ${ORANGE}Migrate down${NO_COLOR}"
echo -e " ${GREEN}${BUILD_DEBUG_match}${NO_COLOR}: ${ORANGE}Build debug${NO_COLOR}"
echo -e " ${GREEN}${RUN_DEBUG_match}${NO_COLOR}: ${ORANGE}Run debug${NO_COLOR}"
echo -e "\n${BRIGHT_PURPLE}* Default\n? Optional${NO_COLOR}"
}
__main() {
_cmd=${1:-help}
if ! [ -z ${1+x} ]; then
shift
fi
case "$_cmd" in
help) __help ;;
"$CLEAN_match") __clean ;;
"$INSTALL_DEPS_match") __install_deps ;;
"$BUILD_match") __build ;;
"$RUN_match") __run "$@" ;;
"$MIGRATE_match") __migrate "$@" ;;
"$BUILD_DEBUG_match") __build_debug ;;
"$RUN_DEBUG_match") __run_debug ;;
"$FIRST_INSTALL_MACRO_match") __first_install_macro "$@" ;;
"$GENERATE_MACRO_match") __generate_macro "$@" ;;
"$INSTALL_PROJECT_TOOLS_match") __install_project_tools ;;
*)
echo -e "Invalid command: $_cmd\n"
__help
;;
esac
__check_missing
}
__main "$@"

10
cmd/main.go Normal file
View File

@ -0,0 +1,10 @@
package main
import (
inf "git.markbailey.dev/cervers/ptpp/infrastructure"
_ "git.markbailey.dev/cervers/ptpp/lib/session/memory"
)
func main() {
inf.NewServer().Serve()
}

View File

@ -0,0 +1,16 @@
-- name: CreateHeartbeat :one
INSERT INTO "heartbeat"
("user", created_at, ip_addr, auth_token)
VALUES ($1, $2, $3, $4)
RETURNING *;
-- name: FindHeartbeatByID :one
SELECT *
FROM "heartbeat"
WHERE id = $1;
-- name: FindHeartbeatByUser :many
SELECT *
FROM "heartbeat"
WHERE "user" = $1
ORDER BY created_at DESC;

View File

@ -0,0 +1,13 @@
-- name: CreateOrganization :one
INSERT INTO "organization"
(name, owner_name, owner_phone, owner_email, created_at, deleted_at, authorized, auth_token)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *;
-- name: FindOrganizationByName :one
SELECT * FROM "organization"
WHERE name = $1;
-- name: FindOrganizationById :one
SELECT * FROM "organization"
WHERE id = $1;

View File

@ -0,0 +1,9 @@
-- name: CreateUser :one
INSERT INTO "user"
(username, password, name, email, auth_token, authorized, admin, organization)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *;
-- name: FindUserByUsername :one
SELECT * FROM "user"
WHERE username = $1;

View File

@ -0,0 +1,16 @@
-- name: CreateHeartbeat :one
INSERT INTO heartbeat
("user", created_at, ip_addr, auth_token)
VALUES (?, ?, ?, ?)
RETURNING *;
-- name: FindHeartbeatByID :one
SELECT *
FROM heartbeat
WHERE id = ?;
-- name: FindHeartbeatByUser :many
SELECT *
FROM heartbeat
WHERE "user" = ?
ORDER BY created_at DESC;

View File

@ -0,0 +1,13 @@
-- name: CreateOrganization :one
INSERT INTO organization
(name, owner_name, owner_phone, owner_email, created_at, deleted_at, authorized, auth_token)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: FindOrganizationByName :one
SELECT * FROM organization
WHERE name = ?;
-- name: FindOrganizationById :one
SELECT * FROM organization
WHERE id = ?;

View File

@ -0,0 +1,9 @@
-- name: CreateUser :one
INSERT INTO "user"
(username, password, name, email, auth_token, authorized, admin, organization)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
RETURNING *;
-- name: FindUserByUsername :one
SELECT * FROM "user"
WHERE username = ?;

View File

@ -0,0 +1,72 @@
DROP TABLE IF EXISTS "heartbeat";
DROP SEQUENCE IF EXISTS heartbeat_id_seq;
CREATE SEQUENCE heartbeat_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
CREATE TABLE "public"."heartbeat"
(
"id" integer DEFAULT nextval('heartbeat_id_seq') NOT NULL,
"user" integer NOT NULL,
"created_at" timestamptz NOT NULL,
"ip_addr" text NOT NULL,
"auth_token" text NOT NULL,
CONSTRAINT "heartbeat_pkey" PRIMARY KEY ("id")
) WITH (oids = false);
CREATE INDEX "heartbeats_user_idx" ON "public"."heartbeat" USING btree ("user");
DROP TABLE IF EXISTS "organization";
DROP SEQUENCE IF EXISTS organization_id_seq;
CREATE SEQUENCE organization_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
CREATE TABLE "public"."organization"
(
"id" integer DEFAULT nextval('organization_id_seq') NOT NULL,
"name" text NOT NULL,
"owner_name" text NOT NULL,
"owner_phone" text NOT NULL,
"owner_email" text NOT NULL,
"created_at" timestamptz NOT NULL,
"deleted_at" timestamptz NULL DEFAULT NULL,
"authorized" integer NOT NULL,
"auth_token" text NOT NULL,
CONSTRAINT "organization_name_key" UNIQUE ("name"),
CONSTRAINT "organization_pkey" PRIMARY KEY ("id")
) WITH (oids = false);
CREATE INDEX "organizations_name_idx" ON "public"."organization" USING btree ("name");
CREATE INDEX "organizations_owner_email_idx" ON "public"."organization" USING btree ("owner_email");
CREATE INDEX "organizations_owner_phone_idx" ON "public"."organization" USING btree ("owner_phone");
DROP TABLE IF EXISTS "user";
DROP SEQUENCE IF EXISTS user_id_seq;
CREATE SEQUENCE user_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
CREATE TABLE "public"."user"
(
"id" integer DEFAULT nextval('user_id_seq') NOT NULL,
"username" text NOT NULL,
"password" text NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
"auth_token" text NOT NULL,
"authorized" integer NOT NULL,
"admin" integer NOT NULL,
"organization" integer NOT NULL,
CONSTRAINT "user_email_key" UNIQUE ("email"),
CONSTRAINT "user_pkey" PRIMARY KEY ("id"),
CONSTRAINT "user_username_key" UNIQUE ("username")
) WITH (oids = false);
CREATE INDEX "user_email_idx" ON "public"."user" USING btree ("email");
CREATE INDEX "user_username_idx" ON "public"."user" USING btree ("username");
ALTER TABLE ONLY "public"."heartbeat"
ADD CONSTRAINT "heartbeat_user_fkey" FOREIGN KEY ("user") REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE;
ALTER TABLE ONLY "public"."user"
ADD CONSTRAINT "user_organization_fkey" FOREIGN KEY (organization) REFERENCES organization (id) ON DELETE CASCADE NOT DEFERRABLE;

View File

@ -0,0 +1,45 @@
PRAGMA foreign_keys = ON;
CREATE TABLE "user"
(
id INTEGER PRIMARY KEY NOT NULL,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
auth_token TEXT NOT NULL,
authorized INTEGER NOT NULL,
admin INTEGER NOT NULL,
organization INTEGER NOT NULL,
FOREIGN KEY (organization) REFERENCES organization (id) ON UPDATE NO ACTION ON DELETE CASCADE
);
CREATE TABLE heartbeat
(
id INTEGER PRIMARY KEY NOT NULL,
"user" INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
ip_addr TEXT NOT NULL,
auth_token TEXT NOT NULL,
FOREIGN KEY ("user") REFERENCES "user" (id) ON UPDATE NO ACTION ON DELETE CASCADE
);
CREATE TABLE organization
(
id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL UNIQUE,
owner_name TEXT NOT NULL,
owner_phone TEXT NOT NULL,
owner_email TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at DATETIME,
authorized INTEGER NOT NULL,
auth_token TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS user_username ON "user" (username);
CREATE INDEX IF NOT EXISTS user_email ON "user" (email);
CREATE INDEX IF NOT EXISTS heartbeats_user ON heartbeat ("user");
CREATE INDEX IF NOT EXISTS organizations_name ON organization (name);
CREATE INDEX IF NOT EXISTS organizations_owner_phone ON organization (owner_phone);
CREATE INDEX IF NOT EXISTS organizations_owner_email ON organization (owner_email);

17
docker-compose.yml Normal file
View File

@ -0,0 +1,17 @@
services:
db:
image: postgres:latest
restart: always
environment:
POSTGRES_USER: ${PG_USER}
POSTGRES_PASSWORD: ${PG_PASS}
POSTGRES_DB: application
ports:
- 5432:5432
volumes:
- ${PG_DATA_DIR}:/var/lib/postgresql/data
adminer:
image: adminer
restart: always
ports:
- 8081:8080

11
go.mod Normal file
View File

@ -0,0 +1,11 @@
module git.markbailey.dev/cervers/ptpp
go 1.23.2
require (
github.com/a-h/templ v0.2.793
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.24
golang.org/x/crypto v0.27.0
)

12
go.sum Normal file
View File

@ -0,0 +1,12 @@
github.com/a-h/templ v0.2.793 h1:Io+/ocnfGWYO4VHdR0zBbf39PQlnzVCVVD+wEEs6/qY=
github.com/a-h/templ v0.2.793/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=

50
handlers/admin/index.go Normal file
View File

@ -0,0 +1,50 @@
package admin
import (
"context"
"errors"
"git.markbailey.dev/cervers/ptpp/view/layout"
"net/http"
"git.markbailey.dev/cervers/ptpp/lib/logger"
"git.markbailey.dev/cervers/ptpp/lib/session"
"git.markbailey.dev/cervers/ptpp/view/admin"
)
type IndexHandler struct {
logger logger.ILogger
session session.IManager
}
func NewAdminIndexHandler(l logger.ILogger, s session.IManager) *IndexHandler {
return &IndexHandler{
logger: l,
session: s,
}
}
func (h IndexHandler) Index(w http.ResponseWriter, r *http.Request) error {
if r.URL.Path != "/" {
w.WriteHeader(http.StatusNotFound)
err := layout.NotFound().Render(context.Background(), w)
if err != nil {
h.logger.Error(h.logger.Wrap(err, "Error rendering 404 page"))
return err
}
return nil
}
username, ok := r.Context().Value("username").(string)
if !ok {
err := errors.New("cannot decode request context: for key \"username\"")
h.logger.Error(h.logger.Wrap(err, "Error decoding request context"))
return err
}
if err := admin.Index(username).Render(context.Background(), w); err != nil {
h.logger.Error(h.logger.Wrap(err, "Error rendering admin index page"))
return err
}
return nil
}

40
handlers/homepage.go Normal file
View File

@ -0,0 +1,40 @@
package handlers
import (
"context"
"git.markbailey.dev/cervers/ptpp/view/layout"
"net/http"
"os"
"git.markbailey.dev/cervers/ptpp/lib/logger"
"git.markbailey.dev/cervers/ptpp/view/homepage"
)
type HomePageHandler struct {
logger logger.ILogger
}
func NewHomePageHandler(l logger.ILogger) *HomePageHandler {
return &HomePageHandler{
logger: l,
}
}
func (h HomePageHandler) Homepage(w http.ResponseWriter, r *http.Request) error {
if r.URL.Path != "/" {
w.WriteHeader(http.StatusNotFound)
err := layout.NotFound().Render(context.Background(), w)
if err != nil {
h.logger.Error(h.logger.Wrap(err, "Error rendering 404 page"))
return err
}
return nil
}
if err := homepage.Homepage(os.Getenv("$HTMX_APP_ENV")).Render(context.Background(), w); err != nil {
h.logger.Error(h.logger.Wrap(err, "Error rendering homepage"))
return err
}
return nil
}

315
handlers/user.go Normal file
View File

@ -0,0 +1,315 @@
package handlers
import (
"context"
"encoding/json"
"git.markbailey.dev/cervers/ptpp/lib/database"
"git.markbailey.dev/cervers/ptpp/lib/database/dto"
"net/http"
"time"
"git.markbailey.dev/cervers/ptpp/lib/logger"
"git.markbailey.dev/cervers/ptpp/lib/session"
"git.markbailey.dev/cervers/ptpp/util"
"git.markbailey.dev/cervers/ptpp/view/user"
)
type UserHandler struct {
logger logger.ILogger
session session.IManager
db database.IDB
}
func NewUserHandler(l logger.ILogger, s session.IManager, db database.IDB) *UserHandler {
return &UserHandler{
logger: l,
session: s,
db: db,
}
}
func (u *UserHandler) Populate(w http.ResponseWriter, r *http.Request) error {
existingOrg, err := u.db.Repo().FindOrganizationByName("CerbervsSoft")
if existingOrg != nil && err == nil {
return util.Redirect(w, r, "/signup", http.StatusSeeOther, false)
}
authToken, err := util.CreateTokenForUser("CerbervsSoft")
if err != nil {
return err
}
organization := dto.Organization{
Name: "CerbervsSoft",
OwnerName: "Mark Bailey",
OwnerEmail: "email@provider.com",
OwnerPhone: "+11111111111",
Authorized: 1,
AuthToken: authToken,
CreatedAt: time.Now(),
}
_, err = u.db.Repo().CreateOrganization(organization)
if err != nil {
return u.logDBError(err)
}
return util.Redirect(w, r, "/signup", http.StatusSeeOther, false)
}
type UserSignInForm struct {
Username string `json:"username"`
Password string `json:"password"`
}
func (u *UserHandler) SignIn(w http.ResponseWriter, r *http.Request) error {
sess := u.session.SessionStart(w, r)
if r.Method == http.MethodGet {
username, ok := r.Context().Value("username").(string)
if ok {
if username == "" {
return u.failWithFormError(w, r, "Invalid Username or Password.")
} else {
foundUser, err := u.db.Repo().FindUserByUsername(username)
if foundUser == nil || err != nil {
return u.failWithFormError(w, r, "Invalid Username or Password.")
}
if foundUser.Admin == 1 {
return util.Redirect(w, r, "/admin/", http.StatusSeeOther, false)
} else {
return util.Redirect(w, r, "/", http.StatusSeeOther, false)
}
}
}
formError, ok := sess.Get("formError").(string)
if !ok {
formError = ""
}
if err := user.SignIn(formError).Render(context.Background(), w); err != nil {
u.logger.Error(u.logger.Wrap(err, "Error rendering sign in form"))
return err
}
return nil
}
fd := &UserSignInForm{}
jsonDecoder := json.NewDecoder(r.Body)
if err := jsonDecoder.Decode(&fd); err != nil {
u.logger.Error(u.logger.Wrap(err, "Error decoding JSON"))
return err
}
foundUser, err := u.db.Repo().FindUserByUsername(fd.Username)
if foundUser == nil || err != nil {
return util.Redirect(w, r, "/signin", http.StatusSeeOther, false)
}
authenticated, err := util.CheckPassword(fd.Password, foundUser.Password)
if err != nil || !authenticated {
return u.failWithFormError(w, r, "Invalid Username or Password.")
}
err = sess.Set("username", foundUser.Username)
if err != nil {
return err
}
token, err := util.CreateTokenForUser(foundUser.Username)
if err != nil {
u.logger.Error(u.logger.Wrap(err, "Error creating token"))
return err
}
err = sess.Set("token", token)
if err != nil {
return err
}
authToken, err := util.CreateTokenForUser(foundUser.Username)
if err != nil {
u.logger.Error(err)
return err
}
_, err = u.db.Repo().CreateHeartbeat(&dto.Heartbeat{
User: foundUser.Identifier,
CreatedAt: time.Now(),
IpAddr: r.RemoteAddr,
AuthToken: authToken,
})
if err != nil {
return u.logDBError(err)
}
jwtToken, err := util.CreateTokenForUser(fd.Username)
if err != nil {
return err
}
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: jwtToken,
Expires: time.Now().Add(time.Hour * 1),
Path: "/",
})
err = sess.Delete("formError")
if err != nil {
return err
}
u.logger.Info(foundUser.Username + " logged in")
if foundUser.Admin == 1 {
return util.Redirect(w, r, "/admin/", http.StatusSeeOther, false)
} else {
return util.Redirect(w, r, "/", http.StatusSeeOther, false)
}
}
type UserSignUpForm struct {
Name string `json:"name"`
Email string `json:"email"`
Username string `json:"username"`
Password string `json:"password"`
PasswordConfirmation string `json:"password_confirmation"`
}
func (u *UserHandler) SignUp(w http.ResponseWriter, r *http.Request) error {
handlerSess := u.session.SessionStart(w, r)
uname, ok := handlerSess.Get("username").(string)
if !ok {
uname = ""
}
if uname != "" {
return util.Redirect(w, r, "/", http.StatusSeeOther, false)
}
if r.Method == http.MethodGet {
if err := user.SignUpForm().Render(context.Background(), w); err != nil {
u.logger.Error(u.logger.Wrap(err, "Error rendering sign up form"))
return err
}
return nil
}
fd := &UserSignUpForm{}
jsonDecoder := json.NewDecoder(r.Body)
if err := jsonDecoder.Decode(&fd); err == nil {
if fd.Password != fd.PasswordConfirmation {
if _, err := w.Write([]byte("Passwords don't match")); err != nil {
u.logger.Error(u.logger.Wrap(err, "Error writing response"))
return err
}
return nil
}
foundUser, err := u.db.Repo().FindUserByUsername(fd.Username)
if foundUser != nil {
if _, err := w.Write([]byte("Invalid username. Please try another")); err != nil {
u.logger.Error(u.logger.Wrap(err, "Error writing response"))
}
return nil
}
token, err := util.CreateTokenForUser(fd.Username)
if err != nil {
u.logger.Error(u.logger.Wrap(err, "Error creating token"))
return err
}
password, err := util.HashPassword(fd.PasswordConfirmation)
if err != nil {
u.logger.Error(u.logger.Wrap(err, "Error hashing password"))
return err
}
org, err := u.db.Repo().FindOrganizationByName("CerbervsSoft")
if org == nil || err != nil {
return u.logDBError(err)
}
newUser, err := u.db.Repo().CreateUser(dto.User{
Username: fd.Username,
Password: password,
Name: fd.Name,
Email: fd.Email,
AuthToken: token,
Authorized: 1,
Admin: 1,
Organization: org.Identifier,
})
if err != nil {
return u.logDBError(err)
}
_, err = u.db.Repo().CreateHeartbeat(&dto.Heartbeat{
User: newUser.Identifier,
CreatedAt: time.Now(),
IpAddr: r.RemoteAddr,
AuthToken: token,
})
if err != nil {
return u.logDBError(err)
}
jwtToken, err := util.CreateTokenForUser(fd.Username)
if err != nil {
return err
}
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: jwtToken,
Expires: time.Now().Add(time.Hour * 1),
Path: "/",
})
return util.Redirect(w, r, "/signin", http.StatusSeeOther, false)
}
return nil
}
func (u *UserHandler) SignOut(w http.ResponseWriter, r *http.Request) error {
sess := u.session.SessionStart(w, r)
err := sess.Delete("username")
if err != nil {
return err
}
err = sess.Delete("token")
if err != nil {
return err
}
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: "",
Expires: time.Now().Add(-time.Hour),
Path: "/",
})
return util.Redirect(w, r, "/", http.StatusSeeOther, true)
}
func (u *UserHandler) logDBError(err error) error {
u.db.Error(err)
return err
}
func (u *UserHandler) failWithFormError(w http.ResponseWriter, r *http.Request, formError string) error {
sess := u.session.SessionStart(w, r)
err := sess.Set("formError", formError)
if err != nil {
return err
}
return util.Redirect(w, r, r.URL.Path, http.StatusSeeOther, false)
}

79
infrastructure/router.go Normal file
View File

@ -0,0 +1,79 @@
package infrastructure
import (
"git.markbailey.dev/cervers/ptpp/lib/database"
"net/http"
"sync"
"git.markbailey.dev/cervers/ptpp/handlers"
"git.markbailey.dev/cervers/ptpp/handlers/admin"
"git.markbailey.dev/cervers/ptpp/lib/logger"
mw "git.markbailey.dev/cervers/ptpp/lib/middleware"
"git.markbailey.dev/cervers/ptpp/lib/session"
)
var (
globalSessions *session.Manager
commonRouter *http.ServeMux
adminStack mw.Func
commonStack mw.Func
lock = &sync.Mutex{}
il logger.ILogger
)
func GetRouter() http.Handler {
commonStack = mw.Compose(
mw.WithLogger,
mw.WithUsername,
)
if commonRouter == nil {
lock.Lock()
defer lock.Unlock()
adminStack = mw.Compose(
mw.WithAuth,
)
commonRouter = http.NewServeMux()
adminRouter := http.NewServeMux()
// Serve static files
fs := http.FileServer(http.Dir("./public/"))
commonRouter.Handle("GET /public/", http.StripPrefix("/public", fs))
commonRouter.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./public/assets/favicon/favicon.ico")
})
// Homepage routes
homepageHandler := handlers.NewHomePageHandler(il)
commonRouter.Handle("/", mw.ErrHandler(homepageHandler.Homepage))
if env == "production" {
return commonStack(commonRouter)
}
// User routes
userHandler := handlers.NewUserHandler(il, globalSessions, database.ChooseDB())
commonRouter.Handle("GET /signup", mw.ErrHandler(userHandler.SignUp))
commonRouter.Handle("POST /signup", mw.ErrHandler(userHandler.SignUp))
commonRouter.Handle("GET /signin", mw.ErrHandler(userHandler.SignIn))
commonRouter.Handle("POST /signin", mw.ErrHandler(userHandler.SignIn))
commonRouter.Handle("GET /signout", mw.ErrHandler(userHandler.SignOut))
commonRouter.Handle("GET /populate", mw.ErrHandler(userHandler.Populate))
// Admin routes
adminIndexHandler := admin.NewAdminIndexHandler(il, globalSessions)
adminRouter.Handle("GET /", mw.ErrHandler(adminIndexHandler.Index))
commonRouter.Handle("/admin/", http.StripPrefix("/admin", adminStack(adminRouter)))
}
return commonStack(commonRouter)
}
func init() {
globalSessions, _ = session.NewManager("memory", "ptpp", 3600)
go globalSessions.GC()
il = logger.NewCompositeLogger()
}

64
infrastructure/server.go Normal file
View File

@ -0,0 +1,64 @@
package infrastructure
import (
"fmt"
"log"
"net/http"
"os"
"strconv"
"time"
)
type Server struct {
addr string
http.Server
port int
}
var (
env = os.Getenv("HTMX_APP_ENV")
)
func NewServer() *Server {
const (
addr = "0.0.0.0"
)
var port int
if env == "production" {
port = 8080
} else {
port = 8080
}
return &Server{
addr,
http.Server{
Addr: addr + ":" + strconv.Itoa(port),
Handler: GetRouter(),
DisableGeneralOptionsHandler: false,
TLSConfig: nil,
ReadTimeout: 5 * time.Second,
ReadHeaderTimeout: 0,
WriteTimeout: 5 * time.Second,
IdleTimeout: 0,
MaxHeaderBytes: 0,
TLSNextProto: nil,
ConnState: nil,
ErrorLog: nil,
BaseContext: nil,
ConnContext: nil,
},
port,
}
}
func (s *Server) Serve() {
fmt.Printf("Starting.\nListening at %s on port %s\n", s.addr, strconv.Itoa(s.port))
serverError := s.ListenAndServe()
if serverError != nil {
log.Fatal(serverError)
}
}

36
lib/database/db.go Normal file
View File

@ -0,0 +1,36 @@
package database
import (
"git.markbailey.dev/cervers/ptpp/lib/database/development"
"git.markbailey.dev/cervers/ptpp/lib/database/production"
"git.markbailey.dev/cervers/ptpp/lib/repository"
"os"
"git.markbailey.dev/cervers/ptpp/lib/logger"
)
type IDB interface {
Info(string)
Warn(string)
Error(err error)
Repo() repository.IRepository
}
func ChooseDB() IDB {
l := logger.NewDBLogger()
if os.Getenv("HTMX_APP_ENV") == "production" {
db, err := production.NewProdDB(l)
if err != nil {
l.Error(l.Wrap(err, "Error creating production database"))
}
return db
}
db, err := development.NewTestDB(l)
if err != nil {
l.Error(l.Wrap(err, "Error creating test database"))
}
return db
}

View File

@ -0,0 +1,59 @@
package development
import (
"database/sql"
"git.markbailey.dev/cervers/ptpp/lib/logger"
"git.markbailey.dev/cervers/ptpp/lib/repository"
"git.markbailey.dev/cervers/ptpp/models/sqlite"
_ "github.com/mattn/go-sqlite3"
"sync"
)
var (
tDB *sql.DB
tDBLock = &sync.Mutex{}
TestQueries *sqlite.Queries
)
type DB struct {
logger logger.ILogger
Repository repository.IRepository
}
func NewTestDB(l logger.ILogger) (*DB, error) {
if tDB == nil {
tDBLock.Lock()
defer tDBLock.Unlock()
db, err := sql.Open("sqlite3", "data/database.db")
if err != nil {
l.Error(l.Wrap(err, "Error opening database connection"))
return nil, err
}
tDB = db
}
if TestQueries == nil {
TestQueries = sqlite.New(tDB)
}
return &DB{
logger: l,
Repository: &Repo{},
}, nil
}
func (d DB) Repo() repository.IRepository {
return d.Repository
}
func (d DB) Info(msg string) {
d.logger.Info(msg)
}
func (d DB) Warn(msg string) {
d.logger.Warn(msg)
}
func (d DB) Error(err error) {
d.logger.Error(d.logger.Wrap(err, "Error in SQLite3 database"))
}

View File

@ -0,0 +1,68 @@
package development
import (
"context"
"git.markbailey.dev/cervers/ptpp/lib/database/dto"
pterror "git.markbailey.dev/cervers/ptpp/lib/error"
"git.markbailey.dev/cervers/ptpp/models/sqlite"
)
type HeartbeatRepository struct{}
func (hr HeartbeatRepository) CreateHeartbeat(h *dto.Heartbeat) (*dto.Heartbeat, error) {
ctx := context.Background()
heartbeat, err := TestQueries.CreateHeartbeat(ctx, sqlite.CreateHeartbeatParams{
User: h.User,
CreatedAt: &h.CreatedAt,
IpAddr: h.IpAddr,
AuthToken: h.AuthToken,
})
if err != nil {
return nil, pterror.Wrap(err, "Error creating heartbeat")
}
return &dto.Heartbeat{
Identifier: heartbeat.Identifier,
User: heartbeat.User,
CreatedAt: *heartbeat.CreatedAt,
IpAddr: heartbeat.IpAddr,
AuthToken: heartbeat.AuthToken,
}, nil
}
func (hr HeartbeatRepository) FindHeartbeatByID(id int64) (*dto.Heartbeat, error) {
ctx := context.Background()
heartbeat, err := TestQueries.FindHeartbeatByID(ctx, id)
if err != nil {
return nil, pterror.Wrap(err, "Error finding heartbeat by ID")
}
return &dto.Heartbeat{
Identifier: heartbeat.Identifier,
User: heartbeat.User,
CreatedAt: *heartbeat.CreatedAt,
IpAddr: heartbeat.IpAddr,
AuthToken: heartbeat.AuthToken,
}, err
}
func (hr HeartbeatRepository) FindHeartbeatByUser(u dto.User) ([]*dto.Heartbeat, error) {
ctx := context.Background()
heartbeats, err := TestQueries.FindHeartbeatByUser(ctx, u.Identifier)
if err != nil {
return nil, pterror.Wrap(err, "Error finding heartbeats by user")
}
var ret []*dto.Heartbeat
for _, h := range heartbeats {
ret = append(ret, &dto.Heartbeat{
Identifier: h.Identifier,
User: h.User,
CreatedAt: *h.CreatedAt,
IpAddr: h.IpAddr,
AuthToken: h.AuthToken,
})
}
return ret, nil
}

View File

@ -0,0 +1,79 @@
package development
import (
"context"
"git.markbailey.dev/cervers/ptpp/lib/database/dto"
pterror "git.markbailey.dev/cervers/ptpp/lib/error"
"git.markbailey.dev/cervers/ptpp/models/sqlite"
)
type OrganizationRepository struct{}
func (ur OrganizationRepository) CreateOrganization(o dto.Organization) (*dto.Organization, error) {
ctx := context.Background()
organization, err := TestQueries.CreateOrganization(ctx, sqlite.CreateOrganizationParams{
Name: o.Name,
OwnerName: o.OwnerName,
OwnerPhone: o.OwnerPhone,
OwnerEmail: o.OwnerEmail,
CreatedAt: &o.CreatedAt,
DeletedAt: o.DeletedAt,
Authorized: o.Authorized,
AuthToken: o.AuthToken,
})
if err != nil {
return nil, pterror.Wrap(err, "Error creating organization")
}
return &dto.Organization{
Identifier: organization.Identifier,
Name: organization.Name,
OwnerName: organization.OwnerName,
OwnerPhone: organization.OwnerPhone,
OwnerEmail: organization.OwnerEmail,
CreatedAt: *organization.CreatedAt,
DeletedAt: organization.DeletedAt,
Authorized: organization.Authorized,
AuthToken: organization.AuthToken,
}, nil
}
func (ur OrganizationRepository) FindOrganizationByName(name string) (*dto.Organization, error) {
ctx := context.Background()
organization, err := TestQueries.FindOrganizationByName(ctx, name)
if err != nil {
return nil, pterror.Wrap(err, "Error finding organization by name")
}
return &dto.Organization{
Identifier: organization.Identifier,
Name: organization.Name,
OwnerName: organization.OwnerName,
OwnerPhone: organization.OwnerPhone,
OwnerEmail: organization.OwnerEmail,
CreatedAt: *organization.CreatedAt,
DeletedAt: organization.DeletedAt,
Authorized: organization.Authorized,
AuthToken: organization.AuthToken,
}, nil
}
func (ur OrganizationRepository) FindOrganizationById(id int64) (*dto.Organization, error) {
ctx := context.Background()
organization, err := TestQueries.FindOrganizationById(ctx, id)
if err != nil {
return nil, pterror.Wrap(err, "Error finding organization by ID")
}
return &dto.Organization{
Identifier: organization.Identifier,
Name: organization.Name,
OwnerName: organization.OwnerName,
OwnerPhone: organization.OwnerPhone,
OwnerEmail: organization.OwnerEmail,
CreatedAt: *organization.CreatedAt,
DeletedAt: organization.DeletedAt,
Authorized: organization.Authorized,
AuthToken: organization.AuthToken,
}, nil
}

View File

@ -0,0 +1,7 @@
package development
type Repo struct {
OrganizationRepository
UserRepository
HeartbeatRepository
}

View File

@ -0,0 +1,59 @@
package development
import (
"context"
"git.markbailey.dev/cervers/ptpp/lib/database/dto"
pterror "git.markbailey.dev/cervers/ptpp/lib/error"
"git.markbailey.dev/cervers/ptpp/models/sqlite"
)
type UserRepository struct{}
func (ur UserRepository) CreateUser(u dto.User) (*dto.User, error) {
ctx := context.Background()
user, err := TestQueries.CreateUser(ctx, sqlite.CreateUserParams{
Username: u.Username,
Password: u.Password,
Name: u.Name,
Email: u.Email,
AuthToken: u.AuthToken,
Authorized: u.Authorized,
Admin: u.Admin,
Organization: u.Organization,
})
if err != nil {
return nil, pterror.Wrap(err, "Error creating user")
}
return &dto.User{
Identifier: user.Identifier,
Username: user.Username,
Password: user.Password,
Name: user.Name,
Email: user.Email,
AuthToken: user.AuthToken,
Authorized: user.Authorized,
Admin: user.Admin,
Organization: user.Organization,
}, nil
}
func (ur UserRepository) FindUserByUsername(username string) (*dto.User, error) {
ctx := context.Background()
user, err := TestQueries.FindUserByUsername(ctx, username)
if err != nil {
return nil, pterror.Wrap(err, "Error finding user by username")
}
return &dto.User{
Identifier: user.Identifier,
Username: user.Username,
Password: user.Password,
Name: user.Name,
Email: user.Email,
AuthToken: user.AuthToken,
Authorized: user.Authorized,
Admin: user.Admin,
Organization: user.Organization,
}, nil
}

37
lib/database/dto/model.go Normal file
View File

@ -0,0 +1,37 @@
package dto
import (
"time"
)
type Heartbeat struct {
Identifier int64
User int64
CreatedAt time.Time
IpAddr string
AuthToken string
}
type Organization struct {
Identifier int64
Name string
OwnerName string
OwnerPhone string
OwnerEmail string
CreatedAt time.Time
DeletedAt *time.Time
Authorized int64
AuthToken string
}
type User struct {
Identifier int64
Username string
Password string
Name string
Email string
AuthToken string
Authorized int64
Admin int64
Organization int64
}

View File

@ -0,0 +1,60 @@
package production
import (
"database/sql"
"git.markbailey.dev/cervers/ptpp/lib/logger"
"git.markbailey.dev/cervers/ptpp/lib/repository"
"git.markbailey.dev/cervers/ptpp/models/postgres"
_ "github.com/lib/pq"
"os"
"sync"
)
var (
prodDB *sql.DB
prodDBLock = &sync.Mutex{}
ProdQueries *postgres.Queries
)
type DB struct {
logger logger.ILogger
Repository repository.IRepository
}
func NewProdDB(l logger.ILogger) (*DB, error) {
if prodDB == nil {
prodDBLock.Lock()
defer prodDBLock.Unlock()
db, err := sql.Open("postgres", os.Getenv("DB_URL"))
if err != nil {
l.Error(l.Wrap(err, "Error opening database connection"))
return nil, err
}
prodDB = db
}
if ProdQueries == nil {
ProdQueries = postgres.New(prodDB)
}
return &DB{
logger: l,
Repository: &Repo{},
}, nil
}
func (d DB) Repo() repository.IRepository {
return d.Repository
}
func (d DB) Info(msg string) {
d.logger.Info(msg)
}
func (d DB) Warn(msg string) {
d.logger.Warn(msg)
}
func (d DB) Error(err error) {
d.logger.Error(d.logger.Wrap(err, "Error in Postgresql database"))
}

View File

@ -0,0 +1,70 @@
package production
import (
"context"
"git.markbailey.dev/cervers/ptpp/lib/database/dto"
pterror "git.markbailey.dev/cervers/ptpp/lib/error"
"git.markbailey.dev/cervers/ptpp/models/postgres"
)
type HeartbeatRepository struct{}
func (hr HeartbeatRepository) CreateHeartbeat(h *dto.Heartbeat) (*dto.Heartbeat, error) {
ctx := context.Background()
heartbeat, err := ProdQueries.CreateHeartbeat(ctx, postgres.CreateHeartbeatParams{
User: h.User,
CreatedAt: h.CreatedAt,
IpAddr: h.IpAddr,
AuthToken: h.AuthToken,
})
if err != nil {
return nil, pterror.Wrap(err, "Error creating heartbeat")
}
return &dto.Heartbeat{
Identifier: heartbeat.Identifier,
User: heartbeat.User,
CreatedAt: heartbeat.CreatedAt,
IpAddr: heartbeat.IpAddr,
AuthToken: heartbeat.AuthToken,
}, nil
}
func (hr HeartbeatRepository) FindHeartbeatByID(id int64) (*dto.Heartbeat, error) {
ctx := context.Background()
heartbeat, err := ProdQueries.FindHeartbeatByID(ctx, id)
if err != nil {
return nil, pterror.Wrap(err, "Error finding heartbeat by ID")
}
return &dto.Heartbeat{
Identifier: heartbeat.Identifier,
User: heartbeat.User,
CreatedAt: heartbeat.CreatedAt,
IpAddr: heartbeat.IpAddr,
AuthToken: heartbeat.AuthToken,
}, err
}
func (hr HeartbeatRepository) FindHeartbeatByUser(u dto.User) ([]*dto.Heartbeat, error) {
ctx := context.Background()
heartbeat, err := ProdQueries.FindHeartbeatByUser(ctx, u.Identifier)
if err != nil {
return nil, pterror.Wrap(err, "Error finding heartbeat by user")
}
var ret []*dto.Heartbeat
for _, h := range heartbeat {
ret = append(ret, &dto.Heartbeat{
Identifier: h.Identifier,
User: h.User,
CreatedAt: h.CreatedAt,
IpAddr: h.IpAddr,
AuthToken: h.AuthToken,
})
}
return ret, nil
}

View File

@ -0,0 +1,80 @@
package production
import (
"context"
"git.markbailey.dev/cervers/ptpp/lib/database/dto"
pterror "git.markbailey.dev/cervers/ptpp/lib/error"
"git.markbailey.dev/cervers/ptpp/models/postgres"
)
type OrganizationRepository struct{}
func (or OrganizationRepository) CreateOrganization(o dto.Organization) (*dto.Organization, error) {
ctx := context.Background()
organization, err := ProdQueries.CreateOrganization(ctx, postgres.CreateOrganizationParams{
Name: o.Name,
OwnerName: o.OwnerName,
OwnerPhone: o.OwnerPhone,
OwnerEmail: o.OwnerEmail,
CreatedAt: o.CreatedAt,
DeletedAt: o.DeletedAt,
Authorized: o.Authorized,
AuthToken: o.AuthToken,
})
if err != nil {
return nil, err
}
return &dto.Organization{
Identifier: organization.Identifier,
Name: organization.Name,
OwnerName: organization.OwnerName,
OwnerPhone: organization.OwnerPhone,
OwnerEmail: organization.OwnerEmail,
CreatedAt: organization.CreatedAt,
DeletedAt: organization.DeletedAt,
Authorized: organization.Authorized,
AuthToken: organization.AuthToken,
}, nil
}
func (or OrganizationRepository) FindOrganizationByName(name string) (*dto.Organization, error) {
ctx := context.Background()
organization, err := ProdQueries.FindOrganizationByName(ctx, name)
if err != nil {
return nil, pterror.Wrap(err, "Error finding organization by name")
}
return &dto.Organization{
Identifier: organization.Identifier,
Name: organization.Name,
OwnerName: organization.OwnerName,
OwnerPhone: organization.OwnerPhone,
OwnerEmail: organization.OwnerEmail,
CreatedAt: organization.CreatedAt,
DeletedAt: organization.DeletedAt,
Authorized: organization.Authorized,
AuthToken: organization.AuthToken,
}, nil
}
func (or OrganizationRepository) FindOrganizationById(id int64) (*dto.Organization, error) {
ctx := context.Background()
organization, err := ProdQueries.FindOrganizationById(ctx, id)
if err != nil {
return nil, pterror.Wrap(err, "Error finding organization by ID")
}
return &dto.Organization{
Identifier: organization.Identifier,
Name: organization.Name,
OwnerName: organization.OwnerName,
OwnerPhone: organization.OwnerPhone,
OwnerEmail: organization.OwnerEmail,
CreatedAt: organization.CreatedAt,
DeletedAt: organization.DeletedAt,
Authorized: organization.Authorized,
AuthToken: organization.AuthToken,
}, nil
}

View File

@ -0,0 +1,7 @@
package production
type Repo struct {
UserRepository
OrganizationRepository
HeartbeatRepository
}

View File

@ -0,0 +1,59 @@
package production
import (
"context"
"git.markbailey.dev/cervers/ptpp/lib/database/dto"
pterror "git.markbailey.dev/cervers/ptpp/lib/error"
"git.markbailey.dev/cervers/ptpp/models/postgres"
)
type UserRepository struct{}
func (ur UserRepository) CreateUser(u dto.User) (*dto.User, error) {
ctx := context.Background()
user, err := ProdQueries.CreateUser(ctx, postgres.CreateUserParams{
Username: u.Username,
Password: u.Password,
Name: u.Name,
Email: u.Email,
AuthToken: u.AuthToken,
Authorized: u.Authorized,
Admin: u.Admin,
Organization: u.Organization,
})
if err != nil {
return nil, pterror.Wrap(err, "Error creating user")
}
return &dto.User{
Identifier: user.Identifier,
Username: user.Username,
Password: user.Password,
Name: user.Name,
Email: user.Email,
AuthToken: user.AuthToken,
Authorized: user.Authorized,
Admin: user.Admin,
Organization: user.Organization,
}, nil
}
func (ur UserRepository) FindUserByUsername(username string) (*dto.User, error) {
ctx := context.Background()
user, err := ProdQueries.FindUserByUsername(ctx, username)
if err != nil {
return nil, pterror.Wrap(err, "Error finding user by username")
}
return &dto.User{
Identifier: user.Identifier,
Username: user.Username,
Password: user.Password,
Name: user.Name,
Email: user.Email,
AuthToken: user.AuthToken,
Authorized: user.Authorized,
Admin: user.Admin,
Organization: user.Organization,
}, nil
}

16
lib/error/error.go Normal file
View File

@ -0,0 +1,16 @@
package error
import "fmt"
type WrappedError struct {
Err error
Context string
}
func (e *WrappedError) Error() string {
return fmt.Sprintf("%s: %s", e.Context, e.Err.Error())
}
func Wrap(err error, context string) *WrappedError {
return &WrappedError{Err: err, Context: context}
}

33
lib/locator/locator.go Normal file
View File

@ -0,0 +1,33 @@
package locator
import (
perr "git.markbailey.dev/cervers/ptpp/lib/error"
"git.markbailey.dev/cervers/ptpp/lib/locator/service"
"sync"
)
type Locator struct {
services map[string]service.Service
mu sync.RWMutex
}
func (l *Locator) Register(s service.Service) {
l.mu.Lock()
defer l.mu.Unlock()
if l.services == nil {
l.services = make(map[string]service.Service)
}
l.services[s.Name()] = s
}
func (l *Locator) Locate(name string) (service.Service, error) {
l.mu.RLock()
defer l.mu.RUnlock()
s, ok := l.services[name]
if !ok {
return nil, perr.Wrap(service.ErrServiceNotFound{Name: name}, "locator")
}
return s, nil
}

View File

@ -0,0 +1,32 @@
package service
import (
"git.markbailey.dev/cervers/ptpp/lib/database"
"sync"
)
var (
db interface{}
dlock = &sync.Mutex{}
)
type SDB struct{}
func NewSDB() *SDB {
return &SDB{}
}
func (s *SDB) Name() string {
return "db"
}
func (s *SDB) Use() interface{} {
if db == nil {
dlock.Lock()
defer dlock.Unlock()
db = database.ChooseDB()
}
return db
}

View File

@ -0,0 +1,32 @@
package service
import (
"git.markbailey.dev/cervers/ptpp/lib/logger"
"sync"
)
var (
log interface{}
llock = &sync.Mutex{}
)
type SLogger struct{}
func NewSLogger() *SLogger {
return &SLogger{}
}
func (s *SLogger) Name() string {
return "logger"
}
func (s *SLogger) Use() interface{} {
if log == nil {
llock.Lock()
defer llock.Unlock()
log = logger.NewCompositeLogger()
}
return log
}

View File

@ -0,0 +1,16 @@
package service
import "fmt"
type Service interface {
Name() string
Use() interface{}
}
type ErrServiceNotFound struct {
Name string
}
func (e ErrServiceNotFound) Error() string {
return fmt.Sprintf("service %s not found", e.Name)
}

80
lib/logger/composite.go Normal file
View File

@ -0,0 +1,80 @@
package logger
import (
"fmt"
pterror "git.markbailey.dev/cervers/ptpp/lib/error"
"git.markbailey.dev/cervers/ptpp/util"
"log"
"os"
"sync"
)
var (
concreteCompositeLog *Log
compositeLogFile *os.File
compositeLoggerLock = &sync.Mutex{}
compositeLogFileLock = &sync.Mutex{}
)
type CompositeLogger struct{}
func NewCompositeLogger() *CompositeLogger {
return &CompositeLogger{}
}
func (l CompositeLogger) GetLogger() *Log {
if concreteCompositeLog == nil {
compositeLoggerLock.Lock()
defer compositeLoggerLock.Unlock()
logFile := l.getLogFile()
concreteCompositeLog = &Log{
Error: log.New(logFile, "Error:\t", log.Ldate|log.Ltime),
Warn: log.New(logFile, "Warn:\t", log.Ldate|log.Ltime),
Info: log.New(logFile, "Info:\t", log.Ldate|log.Ltime),
}
}
return concreteCompositeLog
}
func (l CompositeLogger) getLogFile() *os.File {
if compositeLogFile == nil {
compositeLogFileLock.Lock()
defer compositeLogFileLock.Unlock()
absPath := util.GetFullyQualifiedPath("/log")
err := os.MkdirAll(absPath, os.ModePerm)
if err != nil {
fmt.Println("Error creating directory:", err)
os.Exit(1)
}
generalLog, err := os.OpenFile(absPath+"/general-log.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
fmt.Println("Error opening file:", err)
os.Exit(1)
}
compositeLogFile = generalLog
}
return compositeLogFile
}
func (l CompositeLogger) Error(e error) {
l.GetLogger().Error.Println(e)
}
func (l CompositeLogger) Warn(w string) {
l.GetLogger().Warn.Println(w)
}
func (l CompositeLogger) Info(i string) {
l.GetLogger().Info.Println(i)
}
func (l CompositeLogger) Wrap(e error, context string) error {
return pterror.Wrap(e, context)
}

75
lib/logger/database.go Normal file
View File

@ -0,0 +1,75 @@
package logger
import (
"fmt"
pterror "git.markbailey.dev/cervers/ptpp/lib/error"
"git.markbailey.dev/cervers/ptpp/util"
"log"
"os"
"sync"
)
var (
dbLog *Log
dbLogFile *os.File
dbLoggerLock = &sync.Mutex{}
dbLogFileLock = &sync.Mutex{}
)
type DBLogger struct{}
func NewDBLogger() *DBLogger {
return &DBLogger{}
}
func (l DBLogger) GetLogger() *Log {
if dbLog == nil {
dbLoggerLock.Lock()
defer dbLoggerLock.Unlock()
logFile := l.getLogFile()
dbLog = &Log{
Error: log.New(logFile, "ERROR:\t", log.Ldate|log.Ltime),
Warn: log.New(logFile, "WARN:\t", log.Ldate|log.Ltime),
Info: log.New(logFile, "INFO:\t", log.Ldate|log.Ltime),
}
}
return dbLog
}
func (l DBLogger) getLogFile() *os.File {
if dbLogFile == nil {
dbLogFileLock.Lock()
defer dbLogFileLock.Unlock()
absPath := util.GetFullyQualifiedPath("/log")
generalLog, err := os.OpenFile(absPath+"/db-log.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
fmt.Println("Error opening file:", err)
os.Exit(1)
}
dbLogFile = generalLog
}
return dbLogFile
}
func (l DBLogger) Error(e error) {
l.GetLogger().Error.Println(e)
}
func (l DBLogger) Warn(w string) {
l.GetLogger().Warn.Println(w)
}
func (l DBLogger) Info(i string) {
l.GetLogger().Info.Println(i)
}
func (l DBLogger) Wrap(e error, context string) error {
return pterror.Wrap(e, context)
}

21
lib/logger/logger.go Normal file
View File

@ -0,0 +1,21 @@
package logger
import (
"log"
"os"
)
type ILogger interface {
GetLogger() *Log
Error(error)
Warn(string)
Info(string)
Wrap(error, string) error
getLogFile() *os.File
}
type Log struct {
Error *log.Logger
Warn *log.Logger
Info *log.Logger
}

View File

@ -0,0 +1,159 @@
package middleware
import (
"fmt"
"git.markbailey.dev/cervers/ptpp/lib/database"
"net/http"
"os"
"git.markbailey.dev/cervers/ptpp/lib/logger"
"git.markbailey.dev/cervers/ptpp/lib/session"
"git.markbailey.dev/cervers/ptpp/util"
)
type Middleware struct {
l logger.ILogger
db database.IDB
}
type (
ErrHandler func(http.ResponseWriter, *http.Request) error
Func func(http.Handler) http.Handler
)
var (
sess session.IManager
)
func (fn ErrHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
w.Header().Set("HX-Retarget", "#layout_content")
w.Header().Set("HX-Reswap", "innerHTML")
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func Compose(xs ...Func) Func {
return func(next http.Handler) http.Handler {
for i := len(xs) - 1; i >= 0; i-- {
x := xs[i]
next = x(next)
}
return next
}
}
func WithLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handlerSess := sess.SessionStart(w, r)
username, ok := handlerSess.Get("username").(string)
if !ok {
username = "<Not Found>"
}
ipAddr := r.Header.Get("X-Real-IP")
if ipAddr == "" {
ipAddr = r.Header.Get("X-Forwarded-For")
}
if ipAddr == "" {
ipAddr = r.RemoteAddr
}
handlerLogger := logger.NewCompositeLogger()
output := fmt.Sprintf(
"%s Request sent from %s to %s (username? %s)",
r.Method,
ipAddr,
r.URL.Path,
username,
)
handlerLogger.Info(output)
next.ServeHTTP(w, r)
})
}
func WithAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var (
claims *util.CustomClaims
cookie *http.Cookie
err error
token string
handlerSess = sess.SessionStart(w, r)
)
if handlerSess.Get("username") != nil {
req := util.AddValuesToRequestContext(r, map[any]any{
"username": handlerSess.Get("username"),
})
next.ServeHTTP(w, req)
return
}
if cookie, err = r.Cookie("token"); err != nil {
_ = util.Redirect(w, r, "/signin", http.StatusSeeOther, true)
return
}
if token = cookie.Value; token == "" {
_ = util.Redirect(w, r, "/signin", http.StatusSeeOther, true)
return
}
if claims, err = util.ParseToken(token, os.Getenv("TOKEN_SECRET")); err != nil {
_ = util.Redirect(w, r, "/signin", http.StatusSeeOther, true)
return
}
req := util.AddValuesToRequestContext(r, map[any]any{
"username": claims.Username,
})
next.ServeHTTP(w, req)
})
}
func WithUsername(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var (
claims *util.CustomClaims
cookie *http.Cookie
err error
token string
handlerSess = sess.SessionStart(w, r)
)
if handlerSess.Get("username") != nil {
req := util.AddValuesToRequestContext(r, map[any]any{
"username": handlerSess.Get("username"),
})
next.ServeHTTP(w, req)
return
}
var uname *string
if cookie, err = r.Cookie("token"); err == nil {
if token = cookie.Value; token == "" {
uname = nil
}
if claims, err = util.ParseToken(token, os.Getenv("TOKEN_SECRET")); err != nil {
uname = nil
}
uname = &claims.Username
}
if uname != nil {
req := util.AddValuesToRequestContext(r, map[any]any{
"username": uname,
})
next.ServeHTTP(w, req)
return
}
next.ServeHTTP(w, r)
})
}
func init() {
sess, _ = session.NewManager("memory", "ptpp", 3600)
}

View File

@ -0,0 +1,26 @@
package repository
import "git.markbailey.dev/cervers/ptpp/lib/database/dto"
type IHeartbeat interface {
CreateHeartbeat(h *dto.Heartbeat) (*dto.Heartbeat, error)
FindHeartbeatByID(id int64) (*dto.Heartbeat, error)
FindHeartbeatByUser(u dto.User) ([]*dto.Heartbeat, error)
}
type IUser interface {
CreateUser(u dto.User) (*dto.User, error)
FindUserByUsername(username string) (*dto.User, error)
}
type IOrganization interface {
CreateOrganization(o dto.Organization) (*dto.Organization, error)
FindOrganizationByName(name string) (*dto.Organization, error)
FindOrganizationById(id int64) (*dto.Organization, error)
}
type IRepository interface {
IHeartbeat
IUser
IOrganization
}

View File

@ -0,0 +1 @@
package memory

View File

@ -0,0 +1,123 @@
package memory
import (
"container/list"
"sync"
"time"
"git.markbailey.dev/cervers/ptpp/lib/session"
)
var prov = &Provider{list: list.New()}
type MemSessionStore struct {
timeAccessed time.Time
value map[interface{}]interface{}
sid string
}
func (m *MemSessionStore) Set(key interface{}, value interface{}) error {
m.value[key] = value
err := prov.SessionUpdate(m.sid)
if err != nil {
return err
}
return nil
}
func (m *MemSessionStore) Get(key interface{}) interface{} {
err := prov.SessionUpdate(m.sid)
if err != nil {
return nil
}
if v, ok := m.value[key]; ok {
return v
}
return nil
}
func (m *MemSessionStore) Delete(key interface{}) error {
delete(m.value, key)
err := prov.SessionUpdate(m.sid)
if err != nil {
return err
}
return nil
}
func (m *MemSessionStore) SessionID() string {
return m.sid
}
type Provider struct {
sessions map[string]*list.Element
list *list.List
lock sync.Mutex
}
func (p *Provider) SessionInit(sid string) (session.ISession, error) {
prov.lock.Lock()
defer prov.lock.Unlock()
v := make(map[interface{}]interface{}, 0)
newSess := &MemSessionStore{sid: sid, timeAccessed: time.Now(), value: v}
elem := prov.list.PushBack(newSess)
prov.sessions[sid] = elem
return newSess, nil
}
func (p *Provider) SessionRead(sid string) (session.ISession, error) {
if element, ok := prov.sessions[sid]; ok {
return element.Value.(*MemSessionStore), nil
}
sess, err := prov.SessionInit(sid)
return sess, err
}
func (p *Provider) SessionDestroy(sid string) error {
if element, ok := prov.sessions[sid]; ok {
delete(prov.sessions, sid)
prov.list.Remove(element)
}
return nil
}
func (p *Provider) SessionGC(maxLifeTime int64) {
prov.lock.Lock()
defer prov.lock.Unlock()
for {
element := prov.list.Back()
if element == nil {
break
}
if (element.Value.(*MemSessionStore).timeAccessed.Unix() + maxLifeTime) < time.Now().Unix() {
prov.list.Remove(element)
delete(prov.sessions, element.Value.(*MemSessionStore).sid)
} else {
break
}
}
}
func (p *Provider) SessionUpdate(sid string) error {
p.lock.Lock()
defer p.lock.Unlock()
if elem, ok := p.sessions[sid]; ok {
elem.Value.(*MemSessionStore).timeAccessed = time.Now()
p.list.MoveToFront(elem)
return nil
}
return nil
}
func init() {
prov.sessions = make(map[string]*list.Element, 0)
session.Register("memory", prov)
}

105
lib/session/session.go Normal file
View File

@ -0,0 +1,105 @@
package session
import (
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"net/http"
"sync"
"time"
)
var (
providers = make(map[string]IProvider)
session *Manager
lock sync.Mutex
)
type Manager struct {
provider IProvider
cookieName string
maxLifetime int64
lock sync.Mutex
}
type IManager interface {
SessionStart(w http.ResponseWriter, r *http.Request) ISession
GC()
}
type IProvider interface {
SessionInit(sid string) (ISession, error)
SessionRead(sid string) (ISession, error)
SessionDestroy(sid string) error
SessionGC(maxLifeTime int64)
}
type ISession interface {
Set(key, value interface{}) error
Get(key interface{}) interface{}
Delete(key interface{}) error
SessionID() string
}
func NewManager(providerName, cookieName string, maxLifetime int64) (*Manager, error) {
provider, ok := providers[providerName]
if !ok {
return nil, fmt.Errorf("session: unknown provider %q", providerName)
}
if session == nil {
lock.Lock()
defer lock.Unlock()
session = &Manager{provider: provider, cookieName: cookieName, maxLifetime: maxLifetime}
}
return session, nil
}
func (m *Manager) SessionStart(w http.ResponseWriter, r *http.Request) ISession {
m.lock.Lock()
defer m.lock.Unlock()
cookie, err := r.Cookie(m.cookieName)
var session ISession
if err != nil || cookie.Value == "" {
sid := m.sessionId()
session, _ = m.provider.SessionInit(sid)
cookie := http.Cookie{Name: m.cookieName, Value: sid, Path: "/", HttpOnly: true, MaxAge: int(m.maxLifetime)}
http.SetCookie(w, &cookie)
} else {
sid := cookie.Value
session, _ = m.provider.SessionRead(sid)
}
return session
}
func (m *Manager) GC() {
m.lock.Lock()
defer m.lock.Unlock()
m.provider.SessionGC(m.maxLifetime)
time.AfterFunc(time.Duration(m.maxLifetime), func() { m.GC() })
}
func (m *Manager) sessionId() string {
b := make([]byte, 32)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return ""
}
return base64.URLEncoding.EncodeToString(b)
}
func Register(name string, provider IProvider) {
if providers[name] != nil {
return
}
if provider == nil {
panic("session: Register provider is nil")
}
providers[name] = provider
}

View File

@ -0,0 +1,79 @@
-- +goose Up
-- +goose StatementBegin
DROP TABLE IF EXISTS "heartbeat";
DROP SEQUENCE IF EXISTS heartbeat_id_seq;
CREATE SEQUENCE heartbeat_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
CREATE TABLE "public"."heartbeat"
(
"id" integer DEFAULT nextval('heartbeat_id_seq') NOT NULL,
"user" integer NOT NULL,
"created_at" timestamptz NOT NULL,
"ip_addr" text NOT NULL,
"auth_token" text NOT NULL,
CONSTRAINT "heartbeat_pkey" PRIMARY KEY ("id")
) WITH (oids = false);
CREATE INDEX "heartbeats_user_idx" ON "public"."heartbeat" USING btree ("user");
DROP TABLE IF EXISTS "organization";
DROP SEQUENCE IF EXISTS organization_id_seq;
CREATE SEQUENCE organization_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
CREATE TABLE "public"."organization"
(
"id" integer DEFAULT nextval('organization_id_seq') NOT NULL,
"name" text NOT NULL,
"owner_name" text NOT NULL,
"owner_phone" text NOT NULL,
"owner_email" text NOT NULL,
"created_at" timestamptz NOT NULL,
"deleted_at" timestamptz NULL DEFAULT NULL,
"authorized" integer NOT NULL,
"auth_token" text NOT NULL,
CONSTRAINT "organization_name_key" UNIQUE ("name"),
CONSTRAINT "organization_pkey" PRIMARY KEY ("id")
) WITH (oids = false);
CREATE INDEX "organizations_name_idx" ON "public"."organization" USING btree ("name");
CREATE INDEX "organizations_owner_email_idx" ON "public"."organization" USING btree ("owner_email");
CREATE INDEX "organizations_owner_phone_idx" ON "public"."organization" USING btree ("owner_phone");
DROP TABLE IF EXISTS "user";
DROP SEQUENCE IF EXISTS user_id_seq;
CREATE SEQUENCE user_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1;
CREATE TABLE "public"."user"
(
"id" integer DEFAULT nextval('user_id_seq') NOT NULL,
"username" text NOT NULL,
"password" text NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
"auth_token" text NOT NULL,
"authorized" integer NOT NULL,
"admin" integer NOT NULL,
"organization" integer NOT NULL,
CONSTRAINT "user_email_key" UNIQUE ("email"),
CONSTRAINT "user_pkey" PRIMARY KEY ("id"),
CONSTRAINT "user_username_key" UNIQUE ("username")
) WITH (oids = false);
CREATE INDEX "user_email_idx" ON "public"."user" USING btree ("email");
CREATE INDEX "user_username_idx" ON "public"."user" USING btree ("username");
ALTER TABLE ONLY "public"."heartbeat"
ADD CONSTRAINT "heartbeat_user_fkey" FOREIGN KEY ("user") REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE;
ALTER TABLE ONLY "public"."user"
ADD CONSTRAINT "user_organization_fkey" FOREIGN KEY (organization) REFERENCES organization (id) ON DELETE CASCADE NOT DEFERRABLE;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
-- +goose StatementEnd

View File

@ -0,0 +1,52 @@
-- +goose Up
-- +goose StatementBegin
PRAGMA foreign_keys = ON;
CREATE TABLE "user"
(
id INTEGER PRIMARY KEY NOT NULL,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
auth_token TEXT NOT NULL,
authorized INTEGER NOT NULL,
admin INTEGER NOT NULL,
organization INTEGER NOT NULL,
FOREIGN KEY (organization) REFERENCES organization (id) ON UPDATE NO ACTION ON DELETE CASCADE
);
CREATE TABLE heartbeat
(
id INTEGER PRIMARY KEY NOT NULL,
"user" INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
ip_addr TEXT NOT NULL,
auth_token TEXT NOT NULL,
FOREIGN KEY ("user") REFERENCES "user" (id) ON UPDATE NO ACTION ON DELETE CASCADE
);
CREATE TABLE organization
(
id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL UNIQUE,
owner_name TEXT NOT NULL,
owner_phone TEXT NOT NULL,
owner_email TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at DATETIME,
authorized INTEGER NOT NULL,
auth_token TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS user_username ON "user" (username);
CREATE INDEX IF NOT EXISTS user_email ON "user" (email);
CREATE INDEX IF NOT EXISTS heartbeats_user ON heartbeat ("user");
CREATE INDEX IF NOT EXISTS organizations_name ON organization (name);
CREATE INDEX IF NOT EXISTS organizations_owner_phone ON organization (owner_phone);
CREATE INDEX IF NOT EXISTS organizations_owner_email ON organization (owner_email);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
-- +goose StatementEnd

31
models/postgres/db.go Normal file
View File

@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package postgres
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@ -0,0 +1,98 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: heartbeat.sql
package postgres
import (
"context"
"time"
)
const createHeartbeat = `-- name: CreateHeartbeat :one
INSERT INTO "heartbeat"
("user", created_at, ip_addr, auth_token)
VALUES ($1, $2, $3, $4)
RETURNING id, "user", created_at, ip_addr, auth_token
`
type CreateHeartbeatParams struct {
User int64 `db:"user" json:"user"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
IpAddr string `db:"ip_addr" json:"ip_addr"`
AuthToken string `db:"auth_token" json:"auth_token"`
}
func (q *Queries) CreateHeartbeat(ctx context.Context, arg CreateHeartbeatParams) (Heartbeat, error) {
row := q.db.QueryRowContext(ctx, createHeartbeat,
arg.User,
arg.CreatedAt,
arg.IpAddr,
arg.AuthToken,
)
var i Heartbeat
err := row.Scan(
&i.Identifier,
&i.User,
&i.CreatedAt,
&i.IpAddr,
&i.AuthToken,
)
return i, err
}
const findHeartbeatByID = `-- name: FindHeartbeatByID :one
SELECT id, "user", created_at, ip_addr, auth_token
FROM "heartbeat"
WHERE id = $1
`
func (q *Queries) FindHeartbeatByID(ctx context.Context, id int64) (Heartbeat, error) {
row := q.db.QueryRowContext(ctx, findHeartbeatByID, id)
var i Heartbeat
err := row.Scan(
&i.Identifier,
&i.User,
&i.CreatedAt,
&i.IpAddr,
&i.AuthToken,
)
return i, err
}
const findHeartbeatByUser = `-- name: FindHeartbeatByUser :many
SELECT id, "user", created_at, ip_addr, auth_token
FROM "heartbeat"
WHERE "user" = $1
ORDER BY created_at DESC
`
func (q *Queries) FindHeartbeatByUser(ctx context.Context, user int64) ([]Heartbeat, error) {
rows, err := q.db.QueryContext(ctx, findHeartbeatByUser, user)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Heartbeat{}
for rows.Next() {
var i Heartbeat
if err := rows.Scan(
&i.Identifier,
&i.User,
&i.CreatedAt,
&i.IpAddr,
&i.AuthToken,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

41
models/postgres/models.go Normal file
View File

@ -0,0 +1,41 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package postgres
import (
"time"
)
type Heartbeat struct {
Identifier int64 `db:"id" json:"id"`
User int64 `db:"user" json:"user"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
IpAddr string `db:"ip_addr" json:"ip_addr"`
AuthToken string `db:"auth_token" json:"auth_token"`
}
type Organization struct {
Identifier int64 `db:"id" json:"id"`
Name string `db:"name" json:"name"`
OwnerName string `db:"owner_name" json:"owner_name"`
OwnerPhone string `db:"owner_phone" json:"owner_phone"`
OwnerEmail string `db:"owner_email" json:"owner_email"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
DeletedAt *time.Time `db:"deleted_at" json:"deleted_at"`
Authorized int64 `db:"authorized" json:"authorized"`
AuthToken string `db:"auth_token" json:"auth_token"`
}
type User struct {
Identifier int64 `db:"id" json:"id"`
Username string `db:"username" json:"username"`
Password string `db:"password" json:"password"`
Name string `db:"name" json:"name"`
Email string `db:"email" json:"email"`
AuthToken string `db:"auth_token" json:"auth_token"`
Authorized int64 `db:"authorized" json:"authorized"`
Admin int64 `db:"admin" json:"admin"`
Organization int64 `db:"organization" json:"organization"`
}

View File

@ -0,0 +1,99 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: organization.sql
package postgres
import (
"context"
"time"
)
const createOrganization = `-- name: CreateOrganization :one
INSERT INTO "organization"
(name, owner_name, owner_phone, owner_email, created_at, deleted_at, authorized, auth_token)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, name, owner_name, owner_phone, owner_email, created_at, deleted_at, authorized, auth_token
`
type CreateOrganizationParams struct {
Name string `db:"name" json:"name"`
OwnerName string `db:"owner_name" json:"owner_name"`
OwnerPhone string `db:"owner_phone" json:"owner_phone"`
OwnerEmail string `db:"owner_email" json:"owner_email"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
DeletedAt *time.Time `db:"deleted_at" json:"deleted_at"`
Authorized int64 `db:"authorized" json:"authorized"`
AuthToken string `db:"auth_token" json:"auth_token"`
}
func (q *Queries) CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error) {
row := q.db.QueryRowContext(ctx, createOrganization,
arg.Name,
arg.OwnerName,
arg.OwnerPhone,
arg.OwnerEmail,
arg.CreatedAt,
arg.DeletedAt,
arg.Authorized,
arg.AuthToken,
)
var i Organization
err := row.Scan(
&i.Identifier,
&i.Name,
&i.OwnerName,
&i.OwnerPhone,
&i.OwnerEmail,
&i.CreatedAt,
&i.DeletedAt,
&i.Authorized,
&i.AuthToken,
)
return i, err
}
const findOrganizationById = `-- name: FindOrganizationById :one
SELECT id, name, owner_name, owner_phone, owner_email, created_at, deleted_at, authorized, auth_token FROM "organization"
WHERE id = $1
`
func (q *Queries) FindOrganizationById(ctx context.Context, id int64) (Organization, error) {
row := q.db.QueryRowContext(ctx, findOrganizationById, id)
var i Organization
err := row.Scan(
&i.Identifier,
&i.Name,
&i.OwnerName,
&i.OwnerPhone,
&i.OwnerEmail,
&i.CreatedAt,
&i.DeletedAt,
&i.Authorized,
&i.AuthToken,
)
return i, err
}
const findOrganizationByName = `-- name: FindOrganizationByName :one
SELECT id, name, owner_name, owner_phone, owner_email, created_at, deleted_at, authorized, auth_token FROM "organization"
WHERE name = $1
`
func (q *Queries) FindOrganizationByName(ctx context.Context, name string) (Organization, error) {
row := q.db.QueryRowContext(ctx, findOrganizationByName, name)
var i Organization
err := row.Scan(
&i.Identifier,
&i.Name,
&i.OwnerName,
&i.OwnerPhone,
&i.OwnerEmail,
&i.CreatedAt,
&i.DeletedAt,
&i.Authorized,
&i.AuthToken,
)
return i, err
}

View File

@ -0,0 +1,76 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: user.sql
package postgres
import (
"context"
)
const createUser = `-- name: CreateUser :one
INSERT INTO "user"
(username, password, name, email, auth_token, authorized, admin, organization)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, username, password, name, email, auth_token, authorized, admin, organization
`
type CreateUserParams struct {
Username string `db:"username" json:"username"`
Password string `db:"password" json:"password"`
Name string `db:"name" json:"name"`
Email string `db:"email" json:"email"`
AuthToken string `db:"auth_token" json:"auth_token"`
Authorized int64 `db:"authorized" json:"authorized"`
Admin int64 `db:"admin" json:"admin"`
Organization int64 `db:"organization" json:"organization"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, createUser,
arg.Username,
arg.Password,
arg.Name,
arg.Email,
arg.AuthToken,
arg.Authorized,
arg.Admin,
arg.Organization,
)
var i User
err := row.Scan(
&i.Identifier,
&i.Username,
&i.Password,
&i.Name,
&i.Email,
&i.AuthToken,
&i.Authorized,
&i.Admin,
&i.Organization,
)
return i, err
}
const findUserByUsername = `-- name: FindUserByUsername :one
SELECT id, username, password, name, email, auth_token, authorized, admin, organization FROM "user"
WHERE username = $1
`
func (q *Queries) FindUserByUsername(ctx context.Context, username string) (User, error) {
row := q.db.QueryRowContext(ctx, findUserByUsername, username)
var i User
err := row.Scan(
&i.Identifier,
&i.Username,
&i.Password,
&i.Name,
&i.Email,
&i.AuthToken,
&i.Authorized,
&i.Admin,
&i.Organization,
)
return i, err
}

31
models/sqlite/db.go Normal file
View File

@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package sqlite
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@ -0,0 +1,98 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: heartbeat.sql
package sqlite
import (
"context"
"time"
)
const createHeartbeat = `-- name: CreateHeartbeat :one
INSERT INTO heartbeat
("user", created_at, ip_addr, auth_token)
VALUES (?, ?, ?, ?)
RETURNING id, user, created_at, ip_addr, auth_token
`
type CreateHeartbeatParams struct {
User int64 `db:"user" json:"user"`
CreatedAt *time.Time `db:"created_at" json:"created_at"`
IpAddr string `db:"ip_addr" json:"ip_addr"`
AuthToken string `db:"auth_token" json:"auth_token"`
}
func (q *Queries) CreateHeartbeat(ctx context.Context, arg CreateHeartbeatParams) (Heartbeat, error) {
row := q.db.QueryRowContext(ctx, createHeartbeat,
arg.User,
arg.CreatedAt,
arg.IpAddr,
arg.AuthToken,
)
var i Heartbeat
err := row.Scan(
&i.Identifier,
&i.User,
&i.CreatedAt,
&i.IpAddr,
&i.AuthToken,
)
return i, err
}
const findHeartbeatByID = `-- name: FindHeartbeatByID :one
SELECT id, user, created_at, ip_addr, auth_token
FROM heartbeat
WHERE id = ?
`
func (q *Queries) FindHeartbeatByID(ctx context.Context, id int64) (Heartbeat, error) {
row := q.db.QueryRowContext(ctx, findHeartbeatByID, id)
var i Heartbeat
err := row.Scan(
&i.Identifier,
&i.User,
&i.CreatedAt,
&i.IpAddr,
&i.AuthToken,
)
return i, err
}
const findHeartbeatByUser = `-- name: FindHeartbeatByUser :many
SELECT id, user, created_at, ip_addr, auth_token
FROM heartbeat
WHERE "user" = ?
ORDER BY created_at DESC
`
func (q *Queries) FindHeartbeatByUser(ctx context.Context, user int64) ([]Heartbeat, error) {
rows, err := q.db.QueryContext(ctx, findHeartbeatByUser, user)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Heartbeat{}
for rows.Next() {
var i Heartbeat
if err := rows.Scan(
&i.Identifier,
&i.User,
&i.CreatedAt,
&i.IpAddr,
&i.AuthToken,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

41
models/sqlite/models.go Normal file
View File

@ -0,0 +1,41 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package sqlite
import (
"time"
)
type Heartbeat struct {
Identifier int64 `db:"id" json:"id"`
User int64 `db:"user" json:"user"`
CreatedAt *time.Time `db:"created_at" json:"created_at"`
IpAddr string `db:"ip_addr" json:"ip_addr"`
AuthToken string `db:"auth_token" json:"auth_token"`
}
type Organization struct {
Identifier int64 `db:"id" json:"id"`
Name string `db:"name" json:"name"`
OwnerName string `db:"owner_name" json:"owner_name"`
OwnerPhone string `db:"owner_phone" json:"owner_phone"`
OwnerEmail string `db:"owner_email" json:"owner_email"`
CreatedAt *time.Time `db:"created_at" json:"created_at"`
DeletedAt *time.Time `db:"deleted_at" json:"deleted_at"`
Authorized int64 `db:"authorized" json:"authorized"`
AuthToken string `db:"auth_token" json:"auth_token"`
}
type User struct {
Identifier int64 `db:"id" json:"id"`
Username string `db:"username" json:"username"`
Password string `db:"password" json:"password"`
Name string `db:"name" json:"name"`
Email string `db:"email" json:"email"`
AuthToken string `db:"auth_token" json:"auth_token"`
Authorized int64 `db:"authorized" json:"authorized"`
Admin int64 `db:"admin" json:"admin"`
Organization int64 `db:"organization" json:"organization"`
}

View File

@ -0,0 +1,99 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: organization.sql
package sqlite
import (
"context"
"time"
)
const createOrganization = `-- name: CreateOrganization :one
INSERT INTO organization
(name, owner_name, owner_phone, owner_email, created_at, deleted_at, authorized, auth_token)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id, name, owner_name, owner_phone, owner_email, created_at, deleted_at, authorized, auth_token
`
type CreateOrganizationParams struct {
Name string `db:"name" json:"name"`
OwnerName string `db:"owner_name" json:"owner_name"`
OwnerPhone string `db:"owner_phone" json:"owner_phone"`
OwnerEmail string `db:"owner_email" json:"owner_email"`
CreatedAt *time.Time `db:"created_at" json:"created_at"`
DeletedAt *time.Time `db:"deleted_at" json:"deleted_at"`
Authorized int64 `db:"authorized" json:"authorized"`
AuthToken string `db:"auth_token" json:"auth_token"`
}
func (q *Queries) CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error) {
row := q.db.QueryRowContext(ctx, createOrganization,
arg.Name,
arg.OwnerName,
arg.OwnerPhone,
arg.OwnerEmail,
arg.CreatedAt,
arg.DeletedAt,
arg.Authorized,
arg.AuthToken,
)
var i Organization
err := row.Scan(
&i.Identifier,
&i.Name,
&i.OwnerName,
&i.OwnerPhone,
&i.OwnerEmail,
&i.CreatedAt,
&i.DeletedAt,
&i.Authorized,
&i.AuthToken,
)
return i, err
}
const findOrganizationById = `-- name: FindOrganizationById :one
SELECT id, name, owner_name, owner_phone, owner_email, created_at, deleted_at, authorized, auth_token FROM organization
WHERE id = ?
`
func (q *Queries) FindOrganizationById(ctx context.Context, id int64) (Organization, error) {
row := q.db.QueryRowContext(ctx, findOrganizationById, id)
var i Organization
err := row.Scan(
&i.Identifier,
&i.Name,
&i.OwnerName,
&i.OwnerPhone,
&i.OwnerEmail,
&i.CreatedAt,
&i.DeletedAt,
&i.Authorized,
&i.AuthToken,
)
return i, err
}
const findOrganizationByName = `-- name: FindOrganizationByName :one
SELECT id, name, owner_name, owner_phone, owner_email, created_at, deleted_at, authorized, auth_token FROM organization
WHERE name = ?
`
func (q *Queries) FindOrganizationByName(ctx context.Context, name string) (Organization, error) {
row := q.db.QueryRowContext(ctx, findOrganizationByName, name)
var i Organization
err := row.Scan(
&i.Identifier,
&i.Name,
&i.OwnerName,
&i.OwnerPhone,
&i.OwnerEmail,
&i.CreatedAt,
&i.DeletedAt,
&i.Authorized,
&i.AuthToken,
)
return i, err
}

76
models/sqlite/user.sql.go Normal file
View File

@ -0,0 +1,76 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: user.sql
package sqlite
import (
"context"
)
const createUser = `-- name: CreateUser :one
INSERT INTO "user"
(username, password, name, email, auth_token, authorized, admin, organization)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id, username, password, name, email, auth_token, authorized, admin, organization
`
type CreateUserParams struct {
Username string `db:"username" json:"username"`
Password string `db:"password" json:"password"`
Name string `db:"name" json:"name"`
Email string `db:"email" json:"email"`
AuthToken string `db:"auth_token" json:"auth_token"`
Authorized int64 `db:"authorized" json:"authorized"`
Admin int64 `db:"admin" json:"admin"`
Organization int64 `db:"organization" json:"organization"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, createUser,
arg.Username,
arg.Password,
arg.Name,
arg.Email,
arg.AuthToken,
arg.Authorized,
arg.Admin,
arg.Organization,
)
var i User
err := row.Scan(
&i.Identifier,
&i.Username,
&i.Password,
&i.Name,
&i.Email,
&i.AuthToken,
&i.Authorized,
&i.Admin,
&i.Organization,
)
return i, err
}
const findUserByUsername = `-- name: FindUserByUsername :one
SELECT id, username, password, name, email, auth_token, authorized, admin, organization FROM "user"
WHERE username = ?
`
func (q *Queries) FindUserByUsername(ctx context.Context, username string) (User, error) {
row := q.db.QueryRowContext(ctx, findUserByUsername, username)
var i User
err := row.Scan(
&i.Identifier,
&i.Username,
&i.Password,
&i.Name,
&i.Email,
&i.AuthToken,
&i.Authorized,
&i.Admin,
&i.Organization,
)
return i, err
}

1419
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

10
package.json Normal file
View File

@ -0,0 +1,10 @@
{
"devDependencies": {
"@tailwindcss/forms": "^0.5.9",
"tailwindcss": "^3.4.1"
},
"dependencies": {
"htmx.org": "^1.9.10",
"preline": "^2.0.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View File

@ -0,0 +1,83 @@
@startuml
entity User {
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
auth_token TEXT NOT NULL,
authorized INTEGER NOT NULL,
admin INTEGER NOT NULL,
organization INTEGER NOT NULL,
FOREIGN KEY (organization) REFERENCES Organization(id) ON UPDATE NO ACTION ON DELETE CASCADE
}
entity Heartbeat {
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
user INTEGER NOT NULL,
created TEXT NOT NULL,
ip_addr TEXT NOT NULL,
auth_token TEXT NOT NULL,
FOREIGN KEY (user) REFERENCES User(id) ON UPDATE NO ACTION ON DELETE CASCADE
}
entity Organization {
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL UNIQUE,
owner_name TEXT NOT NULL,
owner_phone TEXT NOT NULL,
owner_email TEXT NOT NULL,
created TEXT NOT NULL,
deleted TEXT NULL DEFAULT NULL,
authorized INTEGER NOT NULL,
auth_token TEXT NOT NULL,
}
entity Pattern {
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL UNIQUE,
description TEXT,
expected_delivery_time DATETIME DEFAULT CURRENT_TIMESTAMP,
time_to_complete INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
}
entity PatternSegment {
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
pattern_id INTEGER NOT NULL,
segment TEXT NOT NULL,
description TEXT,
punch_on DATETIME DEFAULT CURRENT_TIMESTAMP,
punch_off DATETIME DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (pattern_id) REFERENCES Pattern(id) ON UPDATE NO ACTION ON DELETE CASCADE
}
entity PatternSegmentDetail {
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
segment_id INTEGER NOT NULL,
details TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (segment_id) REFERENCES PatternSegment(id) ON UPDATE NO ACTION ON DELETE CASCADE
}
entity PatternSegmentInstruction {
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
segment_id INTEGER NOT NULL,
instruction TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (segment_id) REFERENCES PatternSegment(id) ON UPDATE NO ACTION ON DELETE CASCADE
}
Heartbeat::user --> User::id
User::organization --> Organization::id
PatternSegment::pattern_id --> Pattern::id
PatternSegmentDetail::segment_id --> PatternSegment::id
PatternSegmentInstruction::segment_id --> PatternSegment::id
@enduml

164
public/assets/css/index.css Normal file
View File

@ -0,0 +1,164 @@
@import url('https://fonts.googleapis.com/css?family=Montserrat:400,600,700');
@import url('https://fonts.googleapis.com/css?family=Catamaran:400,800');
@tailwind base;
@tailwind components;
@tailwind utilities;
#NOT_FOUND {
.error-container {
text-align: center;
font-size: 106px;
font-family: 'Catamaran', sans-serif;
font-weight: 800;
margin: 70px 15px;
}
.error-container > span {
display: inline-block;
position: relative;
}
.error-container > span.four {
width: 136px;
height: 43px;
border-radius: 999px;
background: linear-gradient(140deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.07) 43%, transparent 44%, transparent 100%),
linear-gradient(105deg, transparent 0%, transparent 40%, rgba(0, 0, 0, 0.06) 41%, rgba(0, 0, 0, 0.07) 76%, transparent 77%, transparent 100%),
linear-gradient(to right, #d89ca4, #e27b7e);
}
.error-container > span.four:before,
.error-container > span.four:after {
content: '';
display: block;
position: absolute;
border-radius: 999px;
}
.error-container > span.four:before {
width: 43px;
height: 156px;
left: 60px;
bottom: -43px;
background: linear-gradient(128deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.07) 40%, transparent 41%, transparent 100%),
linear-gradient(116deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.07) 50%, transparent 51%, transparent 100%),
linear-gradient(to top, #99749D, #B895AB, #CC9AA6, #D7969E, #E0787F);
}
.error-container > span.four:after {
width: 137px;
height: 43px;
transform: rotate(-49.5deg);
left: -18px;
bottom: 36px;
background: linear-gradient(to right, #99749D, #B895AB, #CC9AA6, #D7969E, #E0787F);
}
.error-container > span.zero {
vertical-align: text-top;
width: 156px;
height: 156px;
border-radius: 999px;
background: linear-gradient(-45deg, transparent 0%, rgba(0, 0, 0, 0.06) 50%, transparent 51%, transparent 100%),
linear-gradient(to top right, #99749D, #99749D, #B895AB, #CC9AA6, #D7969E, #ED8687, #ED8687);
overflow: hidden;
animation: bgshadow 5s infinite;
}
.error-container > span.zero:before {
content: '';
display: block;
position: absolute;
transform: rotate(45deg);
width: 90px;
height: 90px;
left: 0;
bottom: 0;
background: transparent linear-gradient(95deg, transparent 0%, transparent 8%, rgba(0, 0, 0, 0.07) 9%, transparent 50%, transparent 100%) linear-gradient(85deg, transparent 0%, transparent 19%, rgba(0, 0, 0, 0.05) 20%, rgba(0, 0, 0, 0.07) 91%, transparent 92%, transparent 100%);
}
.error-container > span.zero:after {
content: '';
display: block;
position: absolute;
border-radius: 999px;
width: 70px;
height: 70px;
left: 43px;
bottom: 43px;
background: #FDFAF5;
box-shadow: -2px 2px 2px 0 rgba(0, 0, 0, 0.1);
}
.screen-reader-text {
position: absolute;
top: -9999em;
left: -9999em;
}
@keyframes bgshadow {
0% {
box-shadow: inset -160px 160px 0 5px rgba(0, 0, 0, 0.4);
}
45% {
box-shadow: inset 0 0 0 0 rgba(0, 0, 0, 0.1);
}
55% {
box-shadow: inset 0 0 0 0 rgba(0, 0, 0, 0.1);
}
100% {
box-shadow: inset 160px -160px 0 5px rgba(0, 0, 0, 0.4);
}
}
/* demo stuff */
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
body {
background-color: #FDFAF5;
margin-bottom: 50px;
}
html, button, input, select, textarea {
font-family: 'Montserrat', Helvetica, sans-serif;
color: #bbb;
}
h1 {
text-align: center;
margin: 30px 15px;
}
.zoom-area {
max-width: 490px;
margin: 30px auto 30px;
font-size: 19px;
text-align: center;
}
.link-container {
text-align: center;
}
a.more-link {
text-transform: uppercase;
font-size: 13px;
background-color: #de7e85;
padding: 10px 15px;
border-radius: 0;
color: #fff;
display: inline-block;
margin-right: 5px;
margin-bottom: 5px;
line-height: 1.5;
text-decoration: none;
margin-top: 50px;
letter-spacing: 1px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

22
public/assets/js/index.js Normal file
View File

@ -0,0 +1,22 @@
function ready(fn) {
if (document.readyState !== "loading") {
fn();
return;
}
document.addEventListener("DOMContentLoaded", fn());
}
ready(() => {
document.querySelectorAll("form").forEach((e) => {
e.setAttribute("hx-ext", "json-enc");
});
});
ready(() => {
document
.getElementById("#layout_content")
.addEventListener("htmx:responseError", function (event) {
document.getElementById("#layout_content").innerHTML =
event.detail.xhr.response;
});
});

41
sqlc.yml Normal file
View File

@ -0,0 +1,41 @@
version: "2"
sql:
- engine: "postgresql"
schema: "./database/schema/postgres.sql"
queries: "./database/query/postgres/"
gen:
go:
package: "postgres"
out: "models/postgres"
sql_package: "database/sql"
emit_db_tags: true
emit_json_tags: true
emit_empty_slices: true
emit_pointers_for_null_types: true
- engine: "sqlite"
schema: "./database/schema/sqlite.sql"
queries: "./database/query/sqlite/"
gen:
go:
package: "sqlite"
out: "models/sqlite"
sql_package: "database/sql"
emit_db_tags: true
emit_json_tags: true
emit_empty_slices: true
emit_pointers_for_null_types: true
overrides:
go:
rename:
id: "Identifier"
overrides:
- engine: "postgresql"
nullable: true
db_type: "timestamptz"
go_type:
type: "time.Time"
pointer: true
- engine: "postgresql"
unsigned: false
db_type: "pg_catalog.int4"
go_type: "int64"

7
tailwind.config.js Normal file
View File

@ -0,0 +1,7 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./view/**/*.{templ, go}"],
plugins: [
require("@tailwindcss/forms")
]
}

58
util/auth.go Normal file
View File

@ -0,0 +1,58 @@
package util
import (
"errors"
"golang.org/x/crypto/bcrypt"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
)
type CustomClaims struct {
jwt.RegisteredClaims
Username string `json:"user_id"`
}
func CreateTokenForUser(username string) (string, error) {
claims := CustomClaims{
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "ptpp",
},
username,
}
str, err := jwt.NewWithClaims(jwt.SigningMethodHS512, claims).SignedString([]byte(os.Getenv("TOKEN_SECRET")))
if err != nil {
return "", err
}
return str, nil
}
func ParseToken(token string, secret string) (*CustomClaims, error) {
tk, err := jwt.ParseWithClaims(token, &CustomClaims{}, func(t *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if err != nil {
return nil, err
}
if claims, ok := tk.Claims.(*CustomClaims); ok {
return claims, nil
}
return nil, errors.New("invalid claims")
}
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 8)
return string(bytes), err
}
func CheckPassword(password string, hash string) (bool, error) {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil, err
}

60
util/shared.go Normal file
View File

@ -0,0 +1,60 @@
package util
import (
"context"
"log"
"net/http"
"os"
"path"
"path/filepath"
)
func Redirect(w http.ResponseWriter, r *http.Request, path string, status int, forceRedirect bool) error {
w.Header().Set("HX-Redirect", path)
if forceRedirect {
http.Redirect(w, r, path, status)
}
return nil
}
func AddValuesToRequestContext(r *http.Request, values map[any]any) *http.Request {
req := r
for k, v := range values {
req = req.WithContext(context.WithValue(req.Context(), k, v))
}
return req
}
func GetBasePath() string {
dir, err := os.Executable()
if err != nil {
log.Fatal(err.Error())
}
basePath, err := filepath.Abs(path.Dir(dir))
if err != nil {
log.Fatal(err.Error())
}
return basePath
}
// TODO: refactor this. I went too hard
func GetFullyQualifiedPath(subject string) string {
fqp, err := filepath.Abs(GetBasePath() + prefixPath(subject))
if err != nil {
log.Fatal(err.Error())
}
return fqp
}
func prefixPath(s string) string {
if s[0:1] != "/" {
return s + "/"
}
return s
}

10
view/admin/index.templ Normal file
View File

@ -0,0 +1,10 @@
package admin
import "git.markbailey.dev/cervers/ptpp/view/layout"
templ Index(name string) {
@layout.Layout() {
<h1>Welcome, { name }</h1>
<div>You are an admin in a protected area of the site. Please do not share your password with anyone</div>
}
}

73
view/admin/index_templ.go Normal file
View File

@ -0,0 +1,73 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package admin
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "git.markbailey.dev/cervers/ptpp/view/layout"
func Index(name string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h1>Welcome, ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/admin/index.templ`, Line: 7, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h1><div>You are an admin in a protected area of the site. Please do not share your password with anyone</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,27 @@
package homepage
import "git.markbailey.dev/cervers/ptpp/view/layout"
templ Homepage(env string) {
@layout.Layout() {
<div class="h-screen flex flex-col items-center justify-evenly text-blue-400 text-2xl">
<div>
Welcome to the homepage
</div>
if env == "development" {
<div>
<a href="/signup">
<button class="text-gray-400 text-md border-black rounded-lg bg-gray-300 p-2">
Sign Up
</button>
</a>
<a href="/signin">
<button class="text-gray-400 text-md border-black rounded-lg bg-gray-300 p-2">
Sign In
</button>
</a>
</div>
}
</div>
}
}

View File

@ -0,0 +1,70 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package homepage
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "git.markbailey.dev/cervers/ptpp/view/layout"
func Homepage(env string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"h-screen flex flex-col items-center justify-evenly text-blue-400 text-2xl\"><div>Welcome to the homepage</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if env == "development" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div><a href=\"/signup\"><button class=\"text-gray-400 text-md border-black rounded-lg bg-gray-300 p-2\" disabled>Sign Up</button></a> <a href=\"/signin\"><button class=\"text-gray-400 text-md border-black rounded-lg bg-gray-300 p-2\">Sign In</button></a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

25
view/layout/layout.templ Normal file
View File

@ -0,0 +1,25 @@
package layout
templ Layout() {
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta charset="UTF-8" />
<meta name="viewport" content="device-width, initial-scale=1.0" />
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/json-enc.js"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/response-targets.js"></script>
<link href="/public/assets/css/output.css" rel="stylesheet" />
</head>
<body>
<div id="#layout_content" hx-ext="response-targets" hx-target-5*="this">
{ children... }
</div>
<script type="text/javascript" src="/public/assets/js/index.js"></script>
</body>
</html>
}

View File

@ -0,0 +1,48 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package layout
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func Layout() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html lang=\"en\"><head><title></title><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"device-width, initial-scale=1.0\"><script src=\"https://unpkg.com/htmx.org@1.9.10\"></script><script src=\"https://unpkg.com/htmx.org@1.9.10/dist/ext/json-enc.js\"></script><script src=\"https://unpkg.com/htmx.org/dist/ext/response-targets.js\"></script><link href=\"/public/assets/css/output.css\" rel=\"stylesheet\"></head><body><div id=\"#layout_content\" hx-ext=\"response-targets\" hx-target-5*=\"this\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><script type=\"text/javascript\" src=\"/public/assets/js/index.js\"></script></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,17 @@
package layout
templ NotFound() {
@Layout() {
<div id="NOT_FOUND">
<section class="error-container">
<span class="four"><span class="screen-reader-text">4</span></span>
<span class="zero"><span class="screen-reader-text">0</span></span>
<span class="four"><span class="screen-reader-text">4</span></span>
</section>
<div class="link-container">
Page Not Found!
</div>
</div>
}
}

View File

@ -0,0 +1,58 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package layout
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func NotFound() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div id=\"NOT_FOUND\"><section class=\"error-container\"><span class=\"four\"><span class=\"screen-reader-text\">4</span></span> <span class=\"zero\"><span class=\"screen-reader-text\">0</span></span> <span class=\"four\"><span class=\"screen-reader-text\">4</span></span></section><div class=\"link-container\">Page Not Found!</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

69
view/user/signin.templ Normal file
View File

@ -0,0 +1,69 @@
package user
import "git.markbailey.dev/cervers/ptpp/view/layout"
templ SignIn(err string) {
@layout.Layout() {
<section class="bg-gray-50 dark:bg-gray-900" id="section">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div
class="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700"
>
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Create an account
</h1>
<form
class="space-y-4 md:space-y-6"
action="/signin"
method="post"
hx-post="/signin"
hx-swap="outerHTML"
hx-target="#section"
>
<div>
<label
for="username"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>Username</label>
<input
type="text"
name="username"
id="username"
placeholder="Username"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
required="true"
/>
</div>
<div>
<label
for="password"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>Password</label>
<input
type="password"
name="password"
id="password"
placeholder="••••••••"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
required="true"
/>
</div>
if err != "" {
<div class="bg-gray-500 text-red-400 text-2xl rounded-lg border-black">
{ err }
</div>
}
<button
type="submit"
class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
>
Sign In
</button>
</form>
</div>
</div>
</div>
</section>
}
}

83
view/user/signin_templ.go Normal file
View File

@ -0,0 +1,83 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package user
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "git.markbailey.dev/cervers/ptpp/view/layout"
func SignIn(err string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<section class=\"bg-gray-50 dark:bg-gray-900\" id=\"section\"><div class=\"flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0\"><div class=\"w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700\"><div class=\"p-6 space-y-4 md:space-y-6 sm:p-8\"><h1 class=\"text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white\">Create an account</h1><form class=\"space-y-4 md:space-y-6\" action=\"/signin\" method=\"post\" hx-post=\"/signin\" hx-swap=\"outerHTML\" hx-target=\"#section\"><div><label for=\"username\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Username</label> <input type=\"text\" name=\"username\" id=\"username\" placeholder=\"Username\" class=\"bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" required=\"true\"></div><div><label for=\"password\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Password</label> <input type=\"password\" name=\"password\" id=\"password\" placeholder=\"••••••••\" class=\"bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" required=\"true\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if err != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"bg-gray-500 text-red-400 text-2xl rounded-lg border-black\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(err)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/user/signin.templ`, Line: 54, Col: 14}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button type=\"submit\" class=\"w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800\">Sign In</button></form></div></div></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

66
view/user/signup.templ Normal file
View File

@ -0,0 +1,66 @@
package user
import "git.markbailey.dev/cervers/ptpp/view/layout"
templ SignUpForm() {
@layout.Layout() {
<section class="bg-gray-50 dark:bg-gray-900" id="section">
<div class="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
<div
class="w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Create an account
</h1>
<form class="space-y-4 md:space-y-6" action="/signup" method="post" hx-post="/signup"
hx-swap="outerHTML" hx-target="#section">
<div>
<label for="name" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Name
</label>
<input type="text" name="name" id="email"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
required="true" />
</div>
<div>
<label for="email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Email
</label>
<input type="text" name="email" id="email"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="name@company.com" required="true" />
</div>
<div>
<label for="username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Username
</label>
<input type="text" name="username" id="username"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
required="true" />
</div>
<div>
<label for="password"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
<input type="password" name="password" id="password" placeholder="••••••••"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
required="true" />
</div>
<div>
<label for="password_confirmation"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Password</label>
<input type="password" name="password_confirmation" id="password_confirmation"
placeholder="••••••••"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
required="true" />
</div>
<button type="submit"
class="w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
Sign In
</button>
</form>
</div>
</div>
</div>
</section>
}
}

60
view/user/signup_templ.go Normal file
View File

@ -0,0 +1,60 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package user
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "git.markbailey.dev/cervers/ptpp/view/layout"
func SignUpForm() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<section class=\"bg-gray-50 dark:bg-gray-900\" id=\"section\"><div class=\"flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0\"><div class=\"w-full bg-white rounded-lg shadow dark:border md:mt-0 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700\"><div class=\"p-6 space-y-4 md:space-y-6 sm:p-8\"><h1 class=\"text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white\">Create an account</h1><form class=\"space-y-4 md:space-y-6\" action=\"/signup\" method=\"post\" hx-post=\"/signup\" hx-swap=\"outerHTML\" hx-target=\"#section\"><div><label for=\"name\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Name</label> <input type=\"text\" name=\"name\" id=\"email\" class=\"bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" required=\"true\"></div><div><label for=\"email\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Email</label> <input type=\"text\" name=\"email\" id=\"email\" class=\"bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" placeholder=\"name@company.com\" required=\"true\"></div><div><label for=\"username\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Username</label> <input type=\"text\" name=\"username\" id=\"username\" class=\"bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" required=\"true\"></div><div><label for=\"password\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Password</label> <input type=\"password\" name=\"password\" id=\"password\" placeholder=\"••••••••\" class=\"bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" required=\"true\"></div><div><label for=\"password_confirmation\" class=\"block mb-2 text-sm font-medium text-gray-900 dark:text-white\">Password</label> <input type=\"password\" name=\"password_confirmation\" id=\"password_confirmation\" placeholder=\"••••••••\" class=\"bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\" required=\"true\"></div><button type=\"submit\" class=\"w-full text-white bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800\">Sign In</button></form></div></div></div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -0,0 +1,16 @@
package user
import "git.markbailey.dev/cervers/ptpp/view/layout"
templ SignUpSuccess() {
@layout.Layout() {
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Sign Up Success</h1>
<p>Thank you for signing up. Please check your email to verify your account.</p>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,60 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package user
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "git.markbailey.dev/cervers/ptpp/view/layout"
func SignUpSuccess() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"container\"><div class=\"row\"><div class=\"col-md-12\"><h1>Sign Up Success</h1><p>Thank you for signing up. Please check your email to verify your account.</p></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = layout.Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate