├── frontend ├── .nvmrc ├── .eslintrc.json ├── app │ ├── favicon.ico │ ├── layout.tsx │ ├── globals.css │ └── page.tsx ├── public │ ├── replace.webp │ ├── matthew-lejune-z0qFicnvRYw-unsplash.jpg │ ├── vercel.svg │ └── next.svg ├── next.config.mjs ├── postcss.config.mjs ├── lib │ ├── utils.ts │ └── api │ │ └── apiService.js ├── components │ ├── theme-provider.tsx │ ├── ui │ │ ├── input.tsx │ │ ├── checkbox.tsx │ │ ├── tooltip.tsx │ │ ├── popover.tsx │ │ ├── toggle.tsx │ │ ├── alert.tsx │ │ ├── button.tsx │ │ ├── tabs.tsx │ │ ├── accordion.tsx │ │ ├── calendar.tsx │ │ ├── select.tsx │ │ └── dropdown-menu.tsx │ └── nav.tsx ├── components.json ├── .gitignore ├── tailwind.config.ts ├── tsconfig.json ├── src │ └── context │ │ └── AuthContext.tsx ├── README.md ├── package.json └── tailwind.config.js ├── backend ├── .gitignore ├── .sqlboiler.toml ├── debug.sh ├── models │ ├── boil_view_names.go │ ├── boil_table_names.go │ ├── sqlite3_suites_test.go │ ├── boil_queries.go │ ├── boil_queries_test.go │ ├── boil_types.go │ ├── boil_relationship_test.go │ ├── sqlite_upsert.go │ ├── sqlite3_main_test.go │ ├── boil_suites_test.go │ └── boil_main_test.go ├── helper │ ├── strings.go │ ├── system.go │ ├── error.go │ └── validation.go ├── middlewares │ ├── swagger.go │ ├── jwt.go │ └── gateway.go ├── migrations │ └── sqlite │ │ ├── 001_create_tables.down.sql │ │ └── 001_create_tables.up.sql ├── telegram │ └── telegram.go ├── .air.toml ├── .air.debug.toml ├── db │ └── migrations.go ├── encryption │ └── encryption.go ├── env │ └── env.go ├── auth │ ├── jwt.go │ ├── user.go │ └── auth.go ├── backups │ └── s3.go ├── main.go ├── birthdays │ ├── reminders.go │ └── birthdays.go ├── structs │ └── structs.go ├── go.mod └── docs │ └── swagger.yaml ├── example.png ├── services ├── nginx │ └── run ├── backend │ └── run └── frontend │ ├── finish │ └── run ├── .dockerignore ├── .env.template ├── docker-compose.yml ├── nginx.conf ├── docker-compose-prod.yml ├── prod.Dockerfile ├── local.Dockerfile ├── .github └── workflows │ └── main.yml ├── .gitignore └── README.md /frontend/.nvmrc: -------------------------------------------------------------------------------- 1 | v20.11.1 2 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | hbd.db 3 | .env 4 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreth/hbd/HEAD/example.png -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /backend/.sqlboiler.toml: -------------------------------------------------------------------------------- 1 | # .sqlboiler.toml 2 | [sqlite3] 3 | dbname = "hbd.db" 4 | -------------------------------------------------------------------------------- /services/nginx/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | exec nginx -g "daemon off;" 2>&1 3 | -------------------------------------------------------------------------------- /frontend/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreth/hbd/HEAD/frontend/app/favicon.ico -------------------------------------------------------------------------------- /frontend/public/replace.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreth/hbd/HEAD/frontend/public/replace.webp -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | frontend/node_modules 2 | frontend/.next 3 | frontend/next-env.d.ts 4 | backend/.env 5 | backend/tmp 6 | -------------------------------------------------------------------------------- /frontend/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /backend/debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | # Change to the backend directory 3 | cd "backend" || exit 4 | # run air 5 | air -c .air.debug.toml -d 6 | -------------------------------------------------------------------------------- /frontend/public/matthew-lejune-z0qFicnvRYw-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dreth/hbd/HEAD/frontend/public/matthew-lejune-z0qFicnvRYw-unsplash.jpg -------------------------------------------------------------------------------- /frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /frontend/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | HBD_MASTER_KEY=custom_master_key 2 | HBD_ENABLE_BACKUP=false 3 | HBD_USER_ACCESS_KEY_ID=custom_access_key_id 4 | HBD_USER_SECRET_ACCESS_KEY=custom_secret_access_key 5 | HBD_BUCKET_REGION=custom_bucket_region 6 | HBD_BUCKET_NAME=custome_bucket_name 7 | -------------------------------------------------------------------------------- /services/backend/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | cd /app/backend 3 | 4 | if [ ! -d /app/data ]; then 5 | mkdir -p /app/data 6 | fi 7 | 8 | if [ ! -f /app/data/hbd.db ]; then 9 | touch /app/data/hbd.db 10 | fi 11 | 12 | exec ./main 2>&1 13 | -------------------------------------------------------------------------------- /backend/models/boil_view_names.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.16.2 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | var ViewNames = struct { 7 | }{} 8 | -------------------------------------------------------------------------------- /services/frontend/finish: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | if [ "$DISABLE_FRONTEND" = "true" ]; then 3 | echo "Frontend is disabled, service will not be restarted" 4 | exit 125 # Return a non-zero exit code to stop S6 from restarting 5 | else 6 | exit 0 # Service finished successfully, allow restart if needed 7 | fi 8 | -------------------------------------------------------------------------------- /backend/helper/strings.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | // joinStrings joins the strings with a specified separator. 4 | func JoinStrings(elements []string, separator string) string { 5 | var result string 6 | for i, element := range elements { 7 | if i > 0 { 8 | result += separator 9 | } 10 | result += element 11 | } 12 | return result 13 | } 14 | -------------------------------------------------------------------------------- /services/frontend/run: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | # Check if the frontend is disabled 3 | if [ "$DISABLE_FRONTEND" = "true" ]; then 4 | echo "Frontend is disabled, service will not be restarted" 5 | exit 0 6 | fi 7 | 8 | # Start the frontend 9 | cd /app/frontend 10 | export PORT=${PORT:-8418} 11 | exec npm run start -- --port $PORT --hostname 0.0.0.0 2>&1 12 | -------------------------------------------------------------------------------- /backend/middlewares/swagger.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "hbd/docs" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func SwaggerHostMiddleware() gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | if strings.HasPrefix(c.Request.URL.Path, "/swagger/") { 13 | docs.SwaggerInfo.Host = c.Request.Host 14 | } 15 | c.Next() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/models/boil_table_names.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.16.2 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | var TableNames = struct { 7 | Birthdays string 8 | Users string 9 | }{ 10 | Birthdays: "birthdays", 11 | Users: "users", 12 | } 13 | -------------------------------------------------------------------------------- /frontend/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | import { type ThemeProviderProps } from "next-themes/dist/types" 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children} 9 | } 10 | -------------------------------------------------------------------------------- /backend/models/sqlite3_suites_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.16.2 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import "testing" 7 | 8 | func TestUpsert(t *testing.T) { 9 | t.Run("Birthdays", testBirthdaysUpsert) 10 | 11 | t.Run("Users", testUsersUpsert) 12 | } 13 | -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /backend/migrations/sqlite/001_create_tables.down.sql: -------------------------------------------------------------------------------- 1 | -- Drop the trigger on the users table 2 | DROP TRIGGER IF EXISTS update_users_updated_at; 3 | 4 | -- Drop the trigger on the birthdays table 5 | DROP TRIGGER IF EXISTS update_birthdays_updated_at; 6 | 7 | -- Drop the birthdays table first 8 | DROP TABLE IF EXISTS birthdays; 9 | 10 | -- Drop the users table after dropping the birthdays table 11 | DROP TABLE IF EXISTS users; 12 | -------------------------------------------------------------------------------- /backend/helper/system.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "hbd/structs" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // HealthCheck checks if the service is ready and returns a status response. 10 | // @Summary Check service readiness 11 | // @Description This endpoint checks the readiness of the service and returns a status. 12 | // @Produce json 13 | // @Success 200 {object} structs.Ready 14 | // @Router /health [get] 15 | // @Tags health 16 | func HealthCheck(c *gin.Context) { 17 | c.JSON(200, structs.Ready{Status: "ok"}) 18 | } 19 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /frontend/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /backend/middlewares/jwt.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "hbd/auth" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func JWTAuthMiddleware() gin.HandlerFunc { 12 | return func(c *gin.Context) { 13 | tokenStr := c.GetHeader("Authorization") 14 | if tokenStr == "" { 15 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) 16 | c.Abort() 17 | return 18 | } 19 | 20 | // Remove the "Bearer " part if it exists 21 | tokenStr = strings.TrimPrefix(tokenStr, "Bearer ") 22 | 23 | claims, err := auth.ValidateJWT(tokenStr) 24 | if err != nil { 25 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) 26 | c.Abort() 27 | return 28 | } 29 | 30 | c.Set("Email", claims.Email) 31 | c.Next() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/telegram/telegram.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "log" 5 | 6 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" 7 | ) 8 | 9 | // SendTelegramMessage sends a message via the Telegram bot API. 10 | func SendTelegramMessage(botAPIKey, telegramUserID, message string) { 11 | // Create a new Telegram bot instance using the provided API key 12 | bot, err := tgbotapi.NewBotAPI(botAPIKey) 13 | if err != nil { 14 | log.Println("Error creating Telegram bot:", err) 15 | return 16 | } 17 | 18 | // Create a new message to send to the specified Telegram user/channel 19 | msg := tgbotapi.NewMessageToChannel(telegramUserID, message) 20 | // Send the message via the Telegram bot API 21 | _, err = bot.Send(msg) 22 | if err != nil { 23 | log.Println("Error sending Telegram message:", err) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | hbd: 4 | build: 5 | context: . 6 | dockerfile: local.Dockerfile 7 | container_name: hbd 8 | volumes: 9 | - ./data:/app/data 10 | ports: 11 | - "8418:80" 12 | environment: 13 | - DB_TYPE=sqlite 14 | - DATABASE_URL=/app/data/hbd.db 15 | - MASTER_KEY=${HBD_MASTER_KEY} 16 | - PORT=8418 17 | - ENVIRONMENT=development 18 | - CUSTOM_DOMAIN=https://hbd.lotiguere.com 19 | - GIN_MODE=debug 20 | # Optionally for backups of the birthday database to S3 or S3-compatible services 21 | - HBD_ENABLE_BACKUP=true 22 | - HBD_USER_ACCESS_KEY_ID=${HBD_USER_ACCESS_KEY_ID} 23 | - HBD_USER_SECRET_ACCESS_KEY=${HBD_USER_SECRET_ACCESS_KEY} 24 | - HBD_BUCKET_REGION=${HBD_BUCKET_REGION} 25 | - HBD_BUCKET_NAME=${HBD_BUCKET_NAME} 26 | # Optionally disable the frontend in case you want to directly interact with the API 27 | - DISABLE_FRONTEND=false 28 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | events { worker_connections 1024; } 4 | 5 | http { 6 | include mime.types; 7 | default_type application/octet-stream; 8 | 9 | sendfile on; 10 | keepalive_timeout 65; 11 | 12 | server { 13 | listen 80; 14 | 15 | location /api/ { 16 | proxy_pass http://localhost:8417; # Gin server 17 | proxy_set_header Host $host; 18 | proxy_set_header X-Real-IP $remote_addr; 19 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 20 | proxy_set_header X-Forwarded-Proto $scheme; 21 | } 22 | 23 | location / { 24 | proxy_pass http://localhost:8418; # Next.js server 25 | proxy_set_header Host $host; 26 | proxy_set_header X-Real-IP $remote_addr; 27 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 28 | proxy_set_header X-Forwarded-Proto $scheme; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | 5 | [build] 6 | args_bin = [] 7 | bin = "/tmp/main" 8 | cmd = "go build -buildvcs=false -o /tmp/main ." 9 | delay = 1000 10 | exclude_dir = ["assets", "tmp"] 11 | exclude_file = [] 12 | exclude_regex = ["_test.go"] 13 | exclude_unchanged = false 14 | follow_symlink = false 15 | full_bin = "" 16 | include_dir = [] 17 | include_ext = ["go", "sql"] 18 | include_file = [] 19 | kill_delay = "0s" 20 | log = "build-errors.log" 21 | poll = false 22 | poll_interval = 0 23 | post_cmd = [] 24 | pre_cmd = [] 25 | rerun = false 26 | rerun_delay = 500 27 | send_interrupt = false 28 | stop_on_error = false 29 | 30 | [color] 31 | app = "" 32 | build = "yellow" 33 | main = "magenta" 34 | runner = "green" 35 | watcher = "cyan" 36 | 37 | [log] 38 | main_only = false 39 | time = false 40 | 41 | [misc] 42 | clean_on_exit = false 43 | 44 | [screen] 45 | clear_on_rebuild = false 46 | keep_scroll = true 47 | -------------------------------------------------------------------------------- /frontend/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { ThemeProvider } from "@/components/theme-provider"; 5 | import Nav from "@/components/nav"; 6 | import { AuthProvider } from "@/src/context/AuthContext"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "HBD", 12 | description: "Never forget a birthday again", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: Readonly<{ 18 | children: React.ReactNode; 19 | }>) { 20 | return ( 21 | 22 | 23 | 29 | 30 |