Compare commits
2 Commits
upgrade-ta
...
remove-day
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f84234106 | |||
| a2445a5dd3 |
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.23.5@sha256:e213430692e5c31aba27473cdc84cfff2896d0c097e984bef67b6a44c75a8181 AS builder_go
|
FROM golang:1.23.5@sha256:8c10f21bec412f08f73aa7b97ca5ac5f28a39d8a88030ad8a339fd0a781d72b4 AS builder_go
|
||||||
WORKDIR /web-app-template
|
WORKDIR /web-app-template
|
||||||
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.62.2
|
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.62.2
|
||||||
RUN go install github.com/a-h/templ/cmd/templ@latest
|
RUN go install github.com/a-h/templ/cmd/templ@latest
|
||||||
@@ -21,7 +21,7 @@ COPY . ./
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|
||||||
FROM debian:12.9@sha256:4abf773f2a570e6873259c4e3ba16de6c6268fb571fd46ec80be7c67822823b3
|
FROM debian:12.9@sha256:321341744acb788e251ebd374aecc1a42d60ce65da7bd4ee9207ff6be6686a62
|
||||||
WORKDIR /web-app-template
|
WORKDIR /web-app-template
|
||||||
RUN apt-get update && apt-get install -y ca-certificates && echo "" > .env
|
RUN apt-get update && apt-get install -y ca-certificates && echo "" > .env
|
||||||
COPY migration ./migration
|
COPY migration ./migration
|
||||||
|
|||||||
14
Readme.md
14
Readme.md
@@ -7,11 +7,11 @@ A basic template with authentication to easily host on a VPC.
|
|||||||
|
|
||||||
This template includes everything essential to build an app. It includes the following features:
|
This template includes everything essential to build an app. It includes the following features:
|
||||||
|
|
||||||
- Authentication: Users can login, logout, register and reset their password. (for increased security TOTP is planned aswell.)
|
- Authentication: Users can login, logout, register and reset their password. For increased security TOTP is available aswell.
|
||||||
- Observability: The stack contains an Grafana+Prometheus instance for basic monitoring. You are able to add alerts and get notified on your phone.
|
- Observability: The stack contains an Grafana+Prometheus instance for basic monitoring. You are able to add alerts and get notified on your phone.
|
||||||
- Mail: You are able to send mail with SMTP. You still need an external Mail Server, but a guide on how to set that up with a custom domain is included.
|
- Mail: You are able to send mail with SMTP. You still need an external Mail Server, but a guide on how to set that up with a custom domain is included.
|
||||||
- SSL: This is included by using traefik as reverse proxy. It handles SSL certificates automatically. Furthermore all services are accessible through subdomains.
|
- SSL: This is included by using traefik as reverse proxy. It handles SSL certificates automatically. Furthermore all services are accessible through subdomains. Best thing is, you can add your more with 3 lines of code
|
||||||
- Stack: Tailwindcss + HTMX + GO Backend with templ and sqlite
|
- Actual Stack: Tailwindcss + HTMX + DaisyUI + GO Backend with templ and sqlite
|
||||||
|
|
||||||
|
|
||||||
## Architecture Design Decisions
|
## Architecture Design Decisions
|
||||||
@@ -51,13 +51,13 @@ Instead of implementing authentication from scratch, an external OAuth2 provider
|
|||||||
|
|
||||||
Pros:
|
Pros:
|
||||||
- The Systems of BigTech are probably safer. They have security experts employed.
|
- The Systems of BigTech are probably safer. They have security experts employed.
|
||||||
- The other external system is responsible to prevent credential stuffing attacks, etc.
|
- The other external system needs to prevent credential stuffing attacks, etc.
|
||||||
- Users don't have to create new credentials
|
- Users don't have to create new credentials
|
||||||
Cons:
|
Cons:
|
||||||
- High dependency on those providers
|
- High dependency on those providers
|
||||||
- Single Point of failure (If your account is banned, your application access get's lost as well.)
|
- Single Point of failure (If your account is banned, your application access get's lost as well)
|
||||||
- It's possible that these providers ban the whole application (All users lose access)
|
- It's possible that these providers ban the whole application
|
||||||
- There still needs to be implemented some logic
|
- There still needs to be implemented some logic server side
|
||||||
- Full application integration can be difficult
|
- Full application integration can be difficult
|
||||||
|
|
||||||
#### 3. Using OAuth2 with Keycloak
|
#### 3. Using OAuth2 with Keycloak
|
||||||
|
|||||||
18
input.css
18
input.css
@@ -1,18 +0,0 @@
|
|||||||
@import 'tailwindcss';
|
|
||||||
|
|
||||||
@source './static/**/*.js';
|
|
||||||
@source './template/**/*.templ';
|
|
||||||
|
|
||||||
@theme {
|
|
||||||
--animate-fade: fadeOut 0.25s ease-in;
|
|
||||||
|
|
||||||
@keyframes fadeOut {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -4,8 +4,9 @@
|
|||||||
"description": "Your (almost) independent tech stack to host on a VPC.",
|
"description": "Your (almost) independent tech stack to host on a VPC.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "mkdir -p static/js && cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -i input.css -o static/css/tailwind.css --minify",
|
"build": "mkdir -p static/js && cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -o static/css/tailwind.css --minify",
|
||||||
"watch": "mkdir -p static/js && cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -i input.css -o static/css/tailwind.css --watch"
|
"watch": "mkdir -p static/js && cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss -o static/css/tailwind.css --watch",
|
||||||
|
"test": ""
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
20
tailwind.config.js
Normal file
20
tailwind.config.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./template/**/*.templ", "./static/**/*.js"],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
|
||||||
|
animation: {
|
||||||
|
fade: 'fadeOut 0.25s ease-in',
|
||||||
|
},
|
||||||
|
|
||||||
|
keyframes: _ => ({
|
||||||
|
fadeOut: {
|
||||||
|
'0%': { opacity: '1' },
|
||||||
|
'100%': { opacity: '0' },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ templ DeleteAccountComp() {
|
|||||||
<p class="text-xl text-red-500 mb-4">
|
<p class="text-xl text-red-500 mb-4">
|
||||||
Are you sure you want to delete your account? This action is irreversible.
|
Are you sure you want to delete your account? This action is irreversible.
|
||||||
</p>
|
</p>
|
||||||
<label class="flex items-center gap-2">
|
<label class="input input-bordered flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
class="grow"
|
class="grow"
|
||||||
@@ -24,7 +24,7 @@ templ DeleteAccountComp() {
|
|||||||
autocapitalize="off"
|
autocapitalize="off"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<button class="self-end">
|
<button class="btn btn-error self-end">
|
||||||
Delete Account
|
Delete Account
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package auth
|
|||||||
templ UserComp(user string) {
|
templ UserComp(user string) {
|
||||||
<div id="user-info" class="flex gap-5 items-center">
|
<div id="user-info" class="flex gap-5 items-center">
|
||||||
if user != "" {
|
if user != "" {
|
||||||
<div class="inline-block relative">
|
<div class="group inline-block relative">
|
||||||
<button class="font-semibold py-2 px-4 inline-flex items-center">
|
<button class="font-semibold py-2 px-4 inline-flex items-center">
|
||||||
<span class="mr-1">{ user }</span>
|
<span class="mr-1">{ user }</span>
|
||||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||||
@@ -11,20 +11,20 @@ templ UserComp(user string) {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="absolute hidden group-hover:block w-full">
|
<div class="absolute hidden group-hover:block w-full">
|
||||||
<ul class="w-fit float-right mr-4 p-3">
|
<ul class="menu bg-base-300 rounded-box w-fit float-right mr-4 p-3">
|
||||||
<li class="mb-1">
|
<li class="mb-1">
|
||||||
<a hx-post="/api/auth/signout" hx-target="#user-info">Sign Out</a>
|
<a hx-post="/api/auth/signout" hx-target="#user-info">Sign Out</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="mb-1">
|
<li class="mb-1">
|
||||||
<a href="/auth/change-password">Change Password</a>
|
<a href="/auth/change-password">Change Password</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="/auth/delete-account" class="">Delete Account</a></li>
|
<li><a href="/auth/delete-account" class="text-error">Delete Account</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
<a href="/auth/signup" class="">Sign Up</a>
|
<a href="/auth/signup" class="btn btn-sm">Sign Up</a>
|
||||||
<a href="/auth/signin" class="">Sign In</a>
|
<a href="/auth/signin" class="btn btn-sm">Sign In</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ templ VerifyComp() {
|
|||||||
<p class="text-lg text-center">
|
<p class="text-lg text-center">
|
||||||
Please check your inbox/spam and click on the link to verify your account.
|
Please check your inbox/spam and click on the link to verify your account.
|
||||||
</p>
|
</p>
|
||||||
<button class="mt-8" hx-get="/api/auth/verify-resend" hx-sync="this:drop" hx-swap="outerHTML">
|
<button class="btn mt-8" hx-get="/api/auth/verify-resend" hx-sync="this:drop" hx-swap="outerHTML">
|
||||||
resend verification email
|
resend verification email
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ templ VerifyResponseComp(isVerified bool) {
|
|||||||
<p class="text-lg text-center">
|
<p class="text-lg text-center">
|
||||||
You have completed the verification process. Thank you!
|
You have completed the verification process. Thank you!
|
||||||
</p>
|
</p>
|
||||||
<a class="mt-8" href="/">
|
<a class="btn btn-primary mt-8" href="/">
|
||||||
Go Home
|
Go Home
|
||||||
</a>
|
</a>
|
||||||
} else {
|
} else {
|
||||||
@@ -20,7 +20,7 @@ templ VerifyResponseComp(isVerified bool) {
|
|||||||
<p class="text-lg text-center">
|
<p class="text-lg text-center">
|
||||||
Please try again by sign up process
|
Please try again by sign up process
|
||||||
</p>
|
</p>
|
||||||
<a class="mt-8" href="/auth/signup">
|
<a class="btn btn-primary mt-8" href="/auth/signup">
|
||||||
Sign Up
|
Sign Up
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package template
|
package template
|
||||||
|
|
||||||
templ Index() {
|
templ Index() {
|
||||||
<div class="h-full">
|
<div class="hero bg-base-200 h-full">
|
||||||
<div class="text-center">
|
<div class="hero-content text-center">
|
||||||
<div class="max-w-md">
|
<div class="max-w-md">
|
||||||
<h1 class="text-5xl font-bold">Next Level Workout Tracker</h1>
|
<h1 class="text-5xl font-bold">Next Level Workout Tracker</h1>
|
||||||
<p class="py-6">
|
<p class="py-6">
|
||||||
Ever wanted to track your workouts and see your progress over time? web-app-template is the perfect
|
Ever wanted to track your workouts and see your progress over time? web-app-template is the perfect
|
||||||
solution for you.
|
solution for you.
|
||||||
</p>
|
</p>
|
||||||
<a href="/workout" class="">Get Started</a>
|
<a href="/workout" class="btn btn-primary">Get Started</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,26 +3,25 @@ package template
|
|||||||
templ Layout(slot templ.Component, user templ.Component) {
|
templ Layout(slot templ.Component, user templ.Component) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>web-app-template</title>
|
<title>web-app-template</title>
|
||||||
<link rel="icon" href="/static/favicon.svg" />
|
<link rel="icon" href="/static/favicon.svg" />
|
||||||
<link rel="stylesheet" href="/static/css/tailwind.css" />
|
<link rel="stylesheet" href="/static/css/tailwind.css" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta
|
<meta name="htmx-config" content='{
|
||||||
name="htmx-config"
|
|
||||||
content='{
|
|
||||||
"includeIndicatorStyles": false,
|
"includeIndicatorStyles": false,
|
||||||
"selfRequestsOnly": true,
|
"selfRequestsOnly": true,
|
||||||
"allowScriptTags": false
|
"allowScriptTags": false
|
||||||
}'
|
}' />
|
||||||
/>
|
|
||||||
<script src="/static/js/htmx.min.js"></script>
|
<script src="/static/js/htmx.min.js"></script>
|
||||||
<script src="/static/js/toast.js"></script>
|
<script src="/static/js/toast.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body hx-headers='{"csrf-token": "CSRF_TOKEN"}'>
|
<body hx-headers='{"csrf-token": "CSRF_TOKEN"}'>
|
||||||
<div class="h-screen flex flex-col">
|
<div class="h-screen flex flex-col">
|
||||||
<div class="flex justify-end items-center gap-2 py-1 px-2 h-12 md:gap-10 md:px-10 md:py-2 shadow-sm">
|
<div class="flex justify-end items-center gap-2 py-1 px-2 h-12 md:gap-10 md:px-10 md:py-2 shadow">
|
||||||
<a href="/" class="flex-1 flex gap-2">
|
<a href="/" class="flex-1 flex gap-2">
|
||||||
<img src="/static/favicon.svg" alt="web-app-template logo" />
|
<img src="/static/favicon.svg" alt="web-app-template logo" />
|
||||||
<span>web-app-template</span>
|
<span>web-app-template</span>
|
||||||
@@ -35,11 +34,12 @@ templ Layout(slot templ.Component, user templ.Component) {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="" id="toasts">
|
<div class="toast" id="toasts">
|
||||||
<div class="hidden" id="toast">
|
<div class="hidden alert" id="toast">
|
||||||
New message arrived.
|
New message arrived.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package template
|
|||||||
|
|
||||||
templ NotFound() {
|
templ NotFound() {
|
||||||
<main class="flex h-full justify-center items-center ">
|
<main class="flex h-full justify-center items-center ">
|
||||||
<div class="p-16 rounded-lg">
|
<div class="bg-error p-16 rounded-lg">
|
||||||
<h1 class="text-4xl mb-5">Not Found</h1>
|
<h1 class="text-4xl text-error-content mb-5">Not Found</h1>
|
||||||
<p class="text-lg mb-5">The page you are looking for does not exist.</p>
|
<p class="text-lg text-error-content mb-5">The page you are looking for does not exist.</p>
|
||||||
<a href="/" class="">Go back to home</a>
|
<a href="/" class="btn btn-lg btn-primary">Go back to home</a>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,17 @@ package workout
|
|||||||
|
|
||||||
templ WorkoutComp(currentDate string) {
|
templ WorkoutComp(currentDate string) {
|
||||||
<main class="mx-2">
|
<main class="mx-2">
|
||||||
<form
|
<form class="max-w-xl mx-auto flex flex-col gap-4 justify-center mt-10" hx-post="/api/workout"
|
||||||
class="max-w-xl mx-auto flex flex-col gap-4 justify-center mt-10"
|
hx-target="#workout-placeholder" hx-swap="outerHTML">
|
||||||
hx-post="/api/workout"
|
|
||||||
hx-target="#workout-placeholder"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
>
|
|
||||||
<h2 class="text-4xl mb-8">Track your workout</h2>
|
<h2 class="text-4xl mb-8">Track your workout</h2>
|
||||||
<input id="date" type="date" class="" value={ currentDate } name="date"/>
|
<input id="date" type="date" class="input input-bordered" value={ currentDate } name="date" />
|
||||||
<select class="w-full" name="type">
|
<select class="select select-bordered w-full" name="type">
|
||||||
<option>Push Ups</option>
|
<option>Push Ups</option>
|
||||||
<option>Pull Ups</option>
|
<option>Pull Ups</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="number" class="" placeholder="Sets" name="sets"/>
|
<input type="number" class="input input-bordered" placeholder="Sets" name="sets" />
|
||||||
<input type="number" class="" placeholder="Reps" name="reps"/>
|
<input type="number" class="input input-bordered" placeholder="Reps" name="reps" />
|
||||||
<button class="self-end">Save</button>
|
<button class="btn btn-primary self-end">Save</button>
|
||||||
</form>
|
</form>
|
||||||
<div hx-get="/api/workout" hx-trigger="load"></div>
|
<div hx-get="/api/workout" hx-trigger="load"></div>
|
||||||
</main>
|
</main>
|
||||||
@@ -31,7 +27,7 @@ type Workout struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
templ WorkoutListComp(workouts []Workout) {
|
templ WorkoutListComp(workouts []Workout) {
|
||||||
<div class="overflow-x-auto mx-auto max-w-lg">
|
<div class="overflow-x-auto mx-auto max-w-screen-lg">
|
||||||
<h2 class="text-4xl mt-14 mb-8">Workout history</h2>
|
<h2 class="text-4xl mt-14 mb-8">Workout history</h2>
|
||||||
<table class="table table-auto max-w-full">
|
<table class="table table-auto max-w-full">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
Reference in New Issue
Block a user