chore: initial commit
This commit is contained in:
commit
133944922c
52
.air.toml
Normal file
52
.air.toml
Normal 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
12
.gitignore
vendored
Normal 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
39
Dockerfile
Normal 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
116
README.md
Normal 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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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
394
bin/app
Executable 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
10
cmd/main.go
Normal 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()
|
||||||
|
}
|
16
database/query/postgres/heartbeat.sql
Normal file
16
database/query/postgres/heartbeat.sql
Normal 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;
|
13
database/query/postgres/organization.sql
Normal file
13
database/query/postgres/organization.sql
Normal 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;
|
9
database/query/postgres/user.sql
Normal file
9
database/query/postgres/user.sql
Normal 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;
|
16
database/query/sqlite/heartbeat.sql
Normal file
16
database/query/sqlite/heartbeat.sql
Normal 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;
|
13
database/query/sqlite/organization.sql
Normal file
13
database/query/sqlite/organization.sql
Normal 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 = ?;
|
9
database/query/sqlite/user.sql
Normal file
9
database/query/sqlite/user.sql
Normal 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 = ?;
|
72
database/schema/postgres.sql
Normal file
72
database/schema/postgres.sql
Normal 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;
|
45
database/schema/sqlite.sql
Normal file
45
database/schema/sqlite.sql
Normal 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
17
docker-compose.yml
Normal 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
11
go.mod
Normal 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
12
go.sum
Normal 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
50
handlers/admin/index.go
Normal 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
40
handlers/homepage.go
Normal 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
315
handlers/user.go
Normal 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
79
infrastructure/router.go
Normal 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
64
infrastructure/server.go
Normal 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
36
lib/database/db.go
Normal 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
|
||||||
|
}
|
59
lib/database/development/db.go
Normal file
59
lib/database/development/db.go
Normal 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"))
|
||||||
|
}
|
68
lib/database/development/heartbeatrepo.go
Normal file
68
lib/database/development/heartbeatrepo.go
Normal 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
|
||||||
|
}
|
79
lib/database/development/organizationrepo.go
Normal file
79
lib/database/development/organizationrepo.go
Normal 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
|
||||||
|
}
|
7
lib/database/development/repo.go
Normal file
7
lib/database/development/repo.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package development
|
||||||
|
|
||||||
|
type Repo struct {
|
||||||
|
OrganizationRepository
|
||||||
|
UserRepository
|
||||||
|
HeartbeatRepository
|
||||||
|
}
|
59
lib/database/development/userrepo.go
Normal file
59
lib/database/development/userrepo.go
Normal 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
37
lib/database/dto/model.go
Normal 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
|
||||||
|
}
|
60
lib/database/production/db.go
Normal file
60
lib/database/production/db.go
Normal 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"))
|
||||||
|
}
|
70
lib/database/production/heartbeatrepo.go
Normal file
70
lib/database/production/heartbeatrepo.go
Normal 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
|
||||||
|
}
|
80
lib/database/production/organizationrepo.go
Normal file
80
lib/database/production/organizationrepo.go
Normal 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
|
||||||
|
}
|
7
lib/database/production/repo.go
Normal file
7
lib/database/production/repo.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package production
|
||||||
|
|
||||||
|
type Repo struct {
|
||||||
|
UserRepository
|
||||||
|
OrganizationRepository
|
||||||
|
HeartbeatRepository
|
||||||
|
}
|
59
lib/database/production/userrepo.go
Normal file
59
lib/database/production/userrepo.go
Normal 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
16
lib/error/error.go
Normal 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
33
lib/locator/locator.go
Normal 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
|
||||||
|
}
|
32
lib/locator/service/database.go
Normal file
32
lib/locator/service/database.go
Normal 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
|
||||||
|
}
|
32
lib/locator/service/logger.go
Normal file
32
lib/locator/service/logger.go
Normal 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
|
||||||
|
}
|
16
lib/locator/service/service.go
Normal file
16
lib/locator/service/service.go
Normal 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
80
lib/logger/composite.go
Normal 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
75
lib/logger/database.go
Normal 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
21
lib/logger/logger.go
Normal 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
|
||||||
|
}
|
159
lib/middleware/middleware.go
Normal file
159
lib/middleware/middleware.go
Normal 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)
|
||||||
|
}
|
26
lib/repository/repository.go
Normal file
26
lib/repository/repository.go
Normal 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
|
||||||
|
}
|
1
lib/session/memory/cookieprovider.go
Normal file
1
lib/session/memory/cookieprovider.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package memory
|
123
lib/session/memory/memprovider.go
Normal file
123
lib/session/memory/memprovider.go
Normal 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
105
lib/session/session.go
Normal 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
|
||||||
|
}
|
79
migrations/postgres/110920241037_init.sql
Normal file
79
migrations/postgres/110920241037_init.sql
Normal 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
|
52
migrations/sqlite/110920241037_init.sql
Normal file
52
migrations/sqlite/110920241037_init.sql
Normal 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
31
models/postgres/db.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
98
models/postgres/heartbeat.sql.go
Normal file
98
models/postgres/heartbeat.sql.go
Normal 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
41
models/postgres/models.go
Normal 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"`
|
||||||
|
}
|
99
models/postgres/organization.sql.go
Normal file
99
models/postgres/organization.sql.go
Normal 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
|
||||||
|
}
|
76
models/postgres/user.sql.go
Normal file
76
models/postgres/user.sql.go
Normal 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
31
models/sqlite/db.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
98
models/sqlite/heartbeat.sql.go
Normal file
98
models/sqlite/heartbeat.sql.go
Normal 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
41
models/sqlite/models.go
Normal 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"`
|
||||||
|
}
|
99
models/sqlite/organization.sql.go
Normal file
99
models/sqlite/organization.sql.go
Normal 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
76
models/sqlite/user.sql.go
Normal 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
1419
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
10
package.json
Normal file
10
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
BIN
project-files/10162024-ptpp-stats.png
Normal file
BIN
project-files/10162024-ptpp-stats.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 151 KiB |
BIN
project-files/11052024-ptpp-stats.png
Normal file
BIN
project-files/11052024-ptpp-stats.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 148 KiB |
BIN
project-files/11092024-ptpp-stats.png
Normal file
BIN
project-files/11092024-ptpp-stats.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 516 KiB |
BIN
project-files/entity-diagram.jpg
Normal file
BIN
project-files/entity-diagram.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 166 KiB |
83
project-files/entity-diagram.puml
Normal file
83
project-files/entity-diagram.puml
Normal 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
164
public/assets/css/index.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
BIN
public/assets/favicon/favicon.ico
Normal file
BIN
public/assets/favicon/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
22
public/assets/js/index.js
Normal file
22
public/assets/js/index.js
Normal 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
41
sqlc.yml
Normal 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
7
tailwind.config.js
Normal 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
58
util/auth.go
Normal 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
60
util/shared.go
Normal 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
10
view/admin/index.templ
Normal 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
73
view/admin/index_templ.go
Normal 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
|
27
view/homepage/homepage.templ
Normal file
27
view/homepage/homepage.templ
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
70
view/homepage/homepage_templ.go
Normal file
70
view/homepage/homepage_templ.go
Normal 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
25
view/layout/layout.templ
Normal 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>
|
||||||
|
}
|
48
view/layout/layout_templ.go
Normal file
48
view/layout/layout_templ.go
Normal 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
|
17
view/layout/notfound.templ
Normal file
17
view/layout/notfound.templ
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
58
view/layout/notfound_templ.go
Normal file
58
view/layout/notfound_templ.go
Normal 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
69
view/user/signin.templ
Normal 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
83
view/user/signin_templ.go
Normal 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
66
view/user/signup.templ
Normal 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
60
view/user/signup_templ.go
Normal 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
|
16
view/user/signupsuccess.templ
Normal file
16
view/user/signupsuccess.templ
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
60
view/user/signupsuccess_templ.go
Normal file
60
view/user/signupsuccess_templ.go
Normal 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
|
Loading…
x
Reference in New Issue
Block a user