Compare commits
No commits in common. "4a5bfc3f1066aa5c57f12b81aa3acd93f3408abf" and "0b29602cb870741ea761fd25db1e01fc2ac78956" have entirely different histories.
4a5bfc3f10
...
0b29602cb8
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
|||||||
/flake.lock
|
/flake.lock
|
||||||
/go.sum
|
/go.sum
|
||||||
/.env
|
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "github.com/haydenhargreaves/Potion/internal/app/server"
|
|
||||||
|
|
||||||
const PORT = 3000
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
s := server.Init(PORT).ConfigureAuth().ConnectDatabase().Setup()
|
|
||||||
defer s.DB.Close()
|
|
||||||
|
|
||||||
s.Start()
|
|
||||||
}
|
|
||||||
@ -213,6 +213,61 @@ creation process will take place here
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
This section outlines the authentication requirements for the application. This section
|
||||||
|
is **not** broken down by page, they are simple defined.
|
||||||
|
|
||||||
|
Consider looking into using Google OAuth. Appears to be free and can be implemented pretty easily.
|
||||||
|
Reference [here](https://developers.google.com/identity/protocols/oauth2).
|
||||||
|
|
||||||
|
#### Pages
|
||||||
|
|
||||||
|
- [ ] **Register Page**
|
||||||
|
- [ ] Input form with required details: *name, email and password*
|
||||||
|
- [ ] *Password strength meter**
|
||||||
|
- [ ] User should be directed to the home page when account is created
|
||||||
|
- [ ] User will be logged in
|
||||||
|
- [ ] A notification will be provided to the user indicating success and redirection
|
||||||
|
- [ ] User should see error messages when validation issues occur
|
||||||
|
- [ ] Email already in use
|
||||||
|
- [ ] Passwords do not match
|
||||||
|
- [ ] Server failure (this should never happen)
|
||||||
|
|
||||||
|
- [ ] **Sign In Page**
|
||||||
|
- [ ] Input form with required details: *email, password and forgot password button*
|
||||||
|
- [ ] User should be directed to the home page after signing in
|
||||||
|
- [ ] A notification will be provided to the user indicating success and redirection
|
||||||
|
- [ ] User should see error messages when validation issues occur
|
||||||
|
- [ ] Invalid password
|
||||||
|
- [ ] Server failure (this should never happen)
|
||||||
|
|
||||||
|
'*': Future ideas
|
||||||
|
|
||||||
|
|
||||||
|
#### API Requirements
|
||||||
|
|
||||||
|
- [ ] **Register Page**
|
||||||
|
- [ ] Create a new user in the database
|
||||||
|
- [ ] Password should be stored in the database as a hash
|
||||||
|
- [ ] Ensure that emails are not duplicated
|
||||||
|
- [ ] "Log user in" when account is created
|
||||||
|
- [ ] User should be redirected to the home page on success
|
||||||
|
|
||||||
|
- [ ] **Sign In Page**
|
||||||
|
- [ ] Sign user in and return data to be stored in the session
|
||||||
|
- [ ] Validate password to the hash stored in the DB
|
||||||
|
- [ ] User should be redirected to the home page on success
|
||||||
|
|
||||||
|
- [ ] **Session Management**
|
||||||
|
- [ ] *Uses cookies to store required data**
|
||||||
|
|
||||||
|
'*': Unsure on technical implementation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Database Requirements
|
## Database Requirements
|
||||||
|
|
||||||
This section outlines the specific technical requirements for the database store for
|
This section outlines the specific technical requirements for the database store for
|
||||||
@ -222,7 +277,7 @@ this application. It will describe the required tables, fields, and other expect
|
|||||||
##### Table ID Choice
|
##### Table ID Choice
|
||||||
|
|
||||||
Typically, I like to use UUID's for ID's. However, after some research I have concluded that for this
|
Typically, I like to use UUID's for ID's. However, after some research I have concluded that for this
|
||||||
application, the use of the `SERIAL` or `SERIAL` type will work sufficiently. Security is not a huge
|
application, the use of the `SERIAL` or `BIGSERIAL` type will work sufficiently. Security is not a huge
|
||||||
concern with this application since no major data will be stored, however, measures will still be in
|
concern with this application since no major data will be stored, however, measures will still be in
|
||||||
place (of course).
|
place (of course).
|
||||||
|
|
||||||
@ -239,8 +294,8 @@ also have a list of attributes which are to be implemented at the database level
|
|||||||
data fields will also have a small example object. A more in-depth data structure can be
|
data fields will also have a small example object. A more in-depth data structure can be
|
||||||
found in **OTHER** section.
|
found in **OTHER** section.
|
||||||
|
|
||||||
- [ ] Recipes: Represents a single recipe.
|
- [ ] Recipe: Represents a single recipe.
|
||||||
- [ ] ID (PK) Serial
|
- [ ] ID (PK) BigSerial
|
||||||
- [ ] Title (Unique, Required) string(128)
|
- [ ] Title (Unique, Required) string(128)
|
||||||
- [ ] Description (Required) text
|
- [ ] Description (Required) text
|
||||||
- [ ] Instructions (Required) string(1024)[]
|
- [ ] Instructions (Required) string(1024)[]
|
||||||
@ -249,78 +304,69 @@ found in **OTHER** section.
|
|||||||
- [ ] Duration (Required) JSONB({ "total": int, "prep": int, "cook": int })
|
- [ ] Duration (Required) JSONB({ "total": int, "prep": int, "cook": int })
|
||||||
- [ ] Category (Required) E_Meal (defined in the [enums](#enums-and-types) section)
|
- [ ] Category (Required) E_Meal (defined in the [enums](#enums-and-types) section)
|
||||||
- [ ] Ingredients (Required) JSONB({ "item_a": [{ "name": string, "quantity": string }], "item_b": ... })
|
- [ ] Ingredients (Required) JSONB({ "item_a": [{ "name": string, "quantity": string }], "item_b": ... })
|
||||||
- [ ] UserId (FK: User.Id) Serial
|
- [ ] UserId (FK: User.Id) BigSerial
|
||||||
- [ ] Modified () date/time stamp
|
- [ ] Modified () date/time stamp
|
||||||
- [ ] Created (Required) date/time stamp
|
- [ ] Created (Required) date/time stamp
|
||||||
|
|
||||||
- [ ] Users: Represents a single user.
|
- [ ] User: Represents a single user.
|
||||||
- [ ] ID (PK) Serial
|
- [ ] ID (PK) BigSerial
|
||||||
- [ ] GoogleId (Unique, Required) text
|
|
||||||
- [ ] Name (Required) string(64)
|
- [ ] Name (Required) string(64)
|
||||||
- [ ] Email (Unique, Required) string(128)
|
- [ ] Email (Unique, Required) string(128)
|
||||||
- [ ] ImageURL () text
|
- [ ] Password (Required) string(128) *stored as hash***
|
||||||
- [ ] GoogleRefreshToken () text
|
|
||||||
- [ ] Created (Required) date/time stamp
|
- [ ] Created (Required) date/time stamp
|
||||||
|
|
||||||
- [ ] Sessions: Represents a single user-session.
|
- [ ] Engagement: Represents a single engagement from a single user.
|
||||||
- [ ] ID (PK) Serial
|
- [ ] ID (PK) BigSerial
|
||||||
- [ ] UserId (FK: User.Id, Required) Serial
|
|
||||||
- [ ] Token (Required) text
|
|
||||||
- [ ] Expiration (Required) date/time stamp
|
|
||||||
- [ ] Created (Required) date/time stamp
|
|
||||||
|
|
||||||
- [ ] Engagements: Represents a single engagement from a single user.
|
|
||||||
- [ ] ID (PK) Serial
|
|
||||||
- [ ] Message () text (Used to store any relevant notes, if needed)
|
- [ ] Message () text (Used to store any relevant notes, if needed)
|
||||||
- [ ] Entity (Serial) Serial (Used to relate an entity, if needed)
|
- [ ] Entity (BigSerial) BigSerial (Used to relate an entity, if needed)
|
||||||
- [ ] UserId (FK: User.Id, Required) Serial
|
- [ ] UserId (FK: User.Id, Required) BigSerial
|
||||||
- [ ] Created (Required) date/time stamp
|
- [ ] Created (Required) date/time stamp
|
||||||
|
|
||||||
- [ ] Likes: **Many-to-many** table to represent a list of recipes liked by a user.
|
- [ ] Like: **Many-to-many** table to represent a list of recipes liked by a user.
|
||||||
- [ ] ID (PK) *Composite key***
|
- [ ] ID (PK) *Composite key***
|
||||||
- [ ] UserId (FK: User.Id, Required) Serial
|
- [ ] UserId (FK: User.Id, Required) BigSerial
|
||||||
- [ ] RecipeId (FK: Recipe.Id, Required) Serial
|
- [ ] RecipeId (FK: Recipe.Id, Required) BigSerial
|
||||||
- [ ] Created (Required) date/time stamp
|
- [ ] Created (Required) date/time stamp
|
||||||
|
|
||||||
- [ ] Tags: Represents a single tag that can be had by many recipes.
|
- [ ] Tag: Represents a single tag that can be had by many recipes.
|
||||||
- [ ] ID (PK) Serial
|
- [ ] ID (PK) BigSerial
|
||||||
- [ ] Name (Unique, Required) string(32)
|
- [ ] Name (Unique, Required) string(32)
|
||||||
- [ ] Created (Required) date/time stamp
|
- [ ] Created (Required) date/time stamp
|
||||||
|
|
||||||
- [ ] RecipeTags: **Many-to-many** table to represent a list of tags on a recipe.
|
- [ ] RecipeTag: **Many-to-many** table to represent a list of tags on a recipe.
|
||||||
- [ ] ID (PK) Serial
|
- [ ] ID (PK) BigSerial
|
||||||
- [ ] RecipeId (FK: Recipe.Id, Required) Serial
|
- [ ] RecipeId (FK: Recipe.Id, Required) BigSerial
|
||||||
- [ ] TagId (FK: Tag.Id, Required) Serial
|
- [ ] TagId (FK: Tag.Id, Required) BigSerial
|
||||||
- [ ] Created (Required) date/time stamp
|
- [ ] Created (Required) date/time stamp
|
||||||
|
|
||||||
- [ ] Lists: Represents a single users shopping list.
|
- [ ] List: Represents a single users shopping list.
|
||||||
- [ ] ID (PK) Serial
|
- [ ] ID (PK) BigSerial
|
||||||
- [ ] UserId (FK: User.Id, Required) Serial
|
- [ ] UserId (FK: User.Id, Required) BigSerial
|
||||||
- [ ] Content (Required) JSONB([ { "name": string, "quantity": string }, ... ])
|
- [ ] Content (Required) JSONB([ { "name": string, "quantity": string }, ... ])
|
||||||
- [ ] Created (Required) date/time stamp
|
- [ ] Created (Required) date/time stamp
|
||||||
|
|
||||||
- [ ] Images: Represents a single image used by a single recipe.
|
- [ ] Image: Represents a single image used by a single recipe.
|
||||||
- [ ] ID (PK) Serial
|
- [ ] ID (PK) BigSerial
|
||||||
- [ ] RecipeId (FK: Recipe.Id, Required) Serial
|
- [ ] RecipeId (FK: Recipe.Id, Required) BigSerial
|
||||||
- [ ] Alt (Required) string(128) (alt text for accessibility, same as recipe title)
|
- [ ] Alt (Required) string(128) (alt text for accessibility, same as recipe title)
|
||||||
- [ ] Url (Required) text
|
- [ ] Url (Required) text
|
||||||
- [ ] Created (Required) date/time stamp
|
- [ ] Created (Required) date/time stamp
|
||||||
|
|
||||||
- [ ] Reviews: Represents a single review on a recipe from a single author.
|
- [ ] Review: Represents a single review on a recipe from a single author.
|
||||||
- [ ] ID (PK) Serial
|
- [ ] ID (PK) BigSerial
|
||||||
- [ ] Comment (Required) text
|
- [ ] Comment (Required) text
|
||||||
- [ ] Rating () int(0..5) (Optional b/c nested replies don't need a rating)
|
- [ ] Rating () int(0..5) (Optional b/c nested replies don't need a rating)
|
||||||
- [ ] ReviewId (FK: Review.Id) Serial (This is used for nested replies)
|
- [ ] ReviewId (FK: Review.Id) BigSerial (This is used for nested replies)
|
||||||
- [ ] RecipeId (FK: Recipe.Id, Required) Serial
|
- [ ] RecipeId (FK: Recipe.Id, Required) BigSerial
|
||||||
- [ ] UserId (FK: User.Id, Required) Serial
|
- [ ] UserId (FK: User.Id, Required) BigSerial
|
||||||
- [ ] Created (Required) date/time stamp
|
- [ ] Created (Required) date/time stamp
|
||||||
|
|
||||||
- [ ] Notifications: Represents a single user notification.
|
- [ ] Notification: Represents a single user notification.
|
||||||
- [ ] ID (PK) Serial
|
- [ ] ID (PK) BigSerial
|
||||||
- [ ] UserId (FK: User.Id, Required) Serial
|
- [ ] UserId (FK: User.Id, Required) BigSerial
|
||||||
- [ ] Type (Required) E_Notification
|
- [ ] Type (Required) E_Notification
|
||||||
- [ ] Message (Required) text
|
- [ ] Message (Required) text
|
||||||
- [ ] Entity (Serial) Serial (Used to relate an entity, if needed)
|
- [ ] Entity (BigSerial) BigSerial (Used to relate an entity, if needed)
|
||||||
- [ ] Read (Required, Default: F) boolean
|
- [ ] Read (Required, Default: F) boolean
|
||||||
- [ ] Created (Required) date/time stamp
|
- [ ] Created (Required) date/time stamp
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,6 @@
|
|||||||
templ
|
templ
|
||||||
tailwindcss_4
|
tailwindcss_4
|
||||||
tailwindcss-language-server
|
tailwindcss-language-server
|
||||||
watchman
|
|
||||||
];
|
];
|
||||||
|
|
||||||
# Define the shell that will be executed.
|
# Define the shell that will be executed.
|
||||||
|
|||||||
44
go.mod
44
go.mod
@ -2,46 +2,4 @@ module github.com/haydenhargreaves/Potion
|
|||||||
|
|
||||||
go 1.24.3
|
go 1.24.3
|
||||||
|
|
||||||
require (
|
require github.com/a-h/templ v0.3.898 // indirect
|
||||||
github.com/a-h/templ v0.3.898
|
|
||||||
github.com/gin-gonic/gin v1.10.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
|
||||||
github.com/a-h/templ/examples/integration-gin v0.0.0-20250610141150-9b34663a6ef0 // indirect
|
|
||||||
github.com/bytedance/sonic v1.13.2 // indirect
|
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
|
||||||
github.com/gin-contrib/cors v1.7.5 // indirect
|
|
||||||
github.com/gin-contrib/sessions v1.0.4 // indirect
|
|
||||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
|
||||||
github.com/gorilla/context v1.1.2 // indirect
|
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
|
||||||
github.com/gorilla/sessions v1.4.0 // indirect
|
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
|
||||||
golang.org/x/arch v0.16.0 // indirect
|
|
||||||
golang.org/x/crypto v0.37.0 // indirect
|
|
||||||
golang.org/x/net v0.39.0 // indirect
|
|
||||||
golang.org/x/oauth2 v0.30.0 // indirect
|
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
|
||||||
golang.org/x/text v0.24.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GoogleLogin(ctx *gin.Context) {
|
|
||||||
deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
|
||||||
url := deps.AuthService.GetGoogleAuthUrl()
|
|
||||||
|
|
||||||
ctx.Redirect(http.StatusSeeOther, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GoogleCallback(ctx *gin.Context) {
|
|
||||||
deps := ctx.MustGet("deps").(*domain.InjectedDependencies)
|
|
||||||
|
|
||||||
var (
|
|
||||||
state string = ctx.Query("state")
|
|
||||||
code string = ctx.Query("code")
|
|
||||||
)
|
|
||||||
|
|
||||||
if dbUser, googleUserInfo, err := deps.AuthService.GoogleAuthSuccess(state, code); err != nil {
|
|
||||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
} else {
|
|
||||||
ctx.JSON(http.StatusOK, gin.H{"googleUserInfo": googleUserInfo, "dbUser": dbUser})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
layouts "github.com/haydenhargreaves/Potion/internal/templates/layouts"
|
|
||||||
pages "github.com/haydenhargreaves/Potion/internal/templates/pages"
|
|
||||||
)
|
|
||||||
|
|
||||||
func LoginPage(ctx *gin.Context) {
|
|
||||||
title := "Potion - Login"
|
|
||||||
page := pages.LoginPage()
|
|
||||||
|
|
||||||
ctx.HTML(200, "", layouts.AppLayout(title, page))
|
|
||||||
}
|
|
||||||
0
internal/app/handlers/recipe_handler.go
Normal file
0
internal/app/handlers/recipe_handler.go
Normal file
@ -1 +0,0 @@
|
|||||||
package handlers
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DepedencyInjectionMiddleware injects the dependencies into the context set. This is a middleware
|
|
||||||
// that is used to apply the required services.
|
|
||||||
func DepedencyInjectionMiddleware(deps *domain.InjectedDependencies) gin.HandlerFunc {
|
|
||||||
return func(ctx *gin.Context) {
|
|
||||||
ctx.Set("deps", deps)
|
|
||||||
ctx.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/a-h/templ/examples/integration-gin/gintemplrenderer"
|
|
||||||
"github.com/gin-contrib/cors"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/haydenhargreaves/Potion/internal/app/handlers"
|
|
||||||
"github.com/haydenhargreaves/Potion/internal/app/service"
|
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
|
||||||
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
|
|
||||||
"github.com/haydenhargreaves/Potion/internal/infrastructure/database/repository"
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Server struct {
|
|
||||||
port int
|
|
||||||
Router *gin.Engine
|
|
||||||
config cors.Config
|
|
||||||
DB *sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init initializes the server with the provided port. CORS settings are defined here.
|
|
||||||
// A pointer to a server object is returned which allows for method chaining.
|
|
||||||
func Init(port int) *Server {
|
|
||||||
// TODO: Set this to release in prod
|
|
||||||
gin.SetMode(gin.DebugMode)
|
|
||||||
|
|
||||||
server := &Server{
|
|
||||||
Router: gin.Default(),
|
|
||||||
port: port,
|
|
||||||
config: cors.DefaultConfig(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some stuff for templ rendering
|
|
||||||
htmlRenderer := server.Router.HTMLRender
|
|
||||||
server.Router.HTMLRender = &gintemplrenderer.HTMLTemplRenderer{FallbackHtmlRenderer: htmlRenderer}
|
|
||||||
|
|
||||||
// Disable proxy warnings
|
|
||||||
server.Router.SetTrustedProxies(nil)
|
|
||||||
|
|
||||||
// Setup the CORS settings and active them
|
|
||||||
server.config.AllowAllOrigins = true
|
|
||||||
server.Router.Use(cors.New(server.config))
|
|
||||||
|
|
||||||
return server
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) ConfigureAuth() *Server {
|
|
||||||
err := godotenv.Load(".env")
|
|
||||||
if err != nil {
|
|
||||||
panic("Could not load env file")
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
redirectUrl string = "http://localhost:3000/v1/api/auth/callback"
|
|
||||||
clientId string = os.Getenv("GOOGLE_CLIENT_ID")
|
|
||||||
clientSecret string = os.Getenv("GOOGLE_CLIENT_SECRET")
|
|
||||||
scope []string = []string{
|
|
||||||
"https://www.googleapis.com/auth/userinfo.email",
|
|
||||||
"https://www.googleapis.com/auth/userinfo.profile",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Setup Google OAuth
|
|
||||||
auth.NewGoogleConfig(redirectUrl, clientId, clientSecret, scope)
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) ConnectDatabase() *Server {
|
|
||||||
err := godotenv.Load(".env")
|
|
||||||
if err != nil {
|
|
||||||
panic("Could not load env file")
|
|
||||||
}
|
|
||||||
|
|
||||||
var connUrl string = os.Getenv("DATABASE_URL")
|
|
||||||
|
|
||||||
db, err := sql.Open("postgres", connUrl)
|
|
||||||
if err != nil {
|
|
||||||
panic("Could not connect to database: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = db.Ping(); err != nil {
|
|
||||||
panic("Error pinging database: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
s.DB = db
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the server on the port provided when the server was initialized
|
|
||||||
func (s *Server) Start() {
|
|
||||||
s.Router.Run(fmt.Sprintf(":%d", s.port))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) Setup() *Server {
|
|
||||||
// Initialize and inject dependencies
|
|
||||||
userRepo := repository.NewUserRepository(s.DB)
|
|
||||||
userService := service.NewUserService(userRepo)
|
|
||||||
authService := service.NewAuthService(userRepo)
|
|
||||||
|
|
||||||
deps := &domain.InjectedDependencies{
|
|
||||||
UserService: userService,
|
|
||||||
AuthService: authService,
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Router.Use(DepedencyInjectionMiddleware(deps))
|
|
||||||
|
|
||||||
// Wrap all routes with a version
|
|
||||||
router_v1 := s.Router.Group("/v1")
|
|
||||||
|
|
||||||
// Domain specific routers
|
|
||||||
router_web := router_v1.Group("/web")
|
|
||||||
router_api := router_v1.Group("/api")
|
|
||||||
|
|
||||||
// Static routes
|
|
||||||
router_web.Static("/static", "./web/static")
|
|
||||||
|
|
||||||
// API router endpoints
|
|
||||||
router_api.GET("/", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"message": "Server is active."}) })
|
|
||||||
|
|
||||||
// WEB router endpoints
|
|
||||||
router_web.GET("/login", handlers.LoginPage)
|
|
||||||
|
|
||||||
// Google oauth
|
|
||||||
router_api.GET("/auth/login", handlers.GoogleLogin)
|
|
||||||
router_api.GET("/auth/callback", handlers.GoogleCallback)
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
domainAuth "github.com/haydenhargreaves/Potion/internal/domain/auth"
|
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
|
||||||
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NOTE: HOW THIS WORKS
|
|
||||||
//
|
|
||||||
// I need to store the refresh token along side the user in the DB.
|
|
||||||
// Create a "session token" (cookie) with an expiration and store that in the DB and the session.
|
|
||||||
// Send this session token along with all the requests (stored in the session) and when
|
|
||||||
// authorization is needed, use it.
|
|
||||||
// Once the expiration of MY token expires, prompt the user to log back in.
|
|
||||||
//
|
|
||||||
// So what is the point of the Google refresh token? Well, its not really useful right now, but if
|
|
||||||
// we need to perform Google actions, we can use it to get more access tokens, which are needed for
|
|
||||||
// the Google actions. Currently, I have no need for it, but it will be stored anyway.
|
|
||||||
//
|
|
||||||
// ------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// AuthService implements the domain.AuthService defined in the domain module.
|
|
||||||
type AuthService struct {
|
|
||||||
userRepository domain.UserRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time check to ensure the AuthService implements domain.AuthService
|
|
||||||
var _ domainAuth.AuthService = (*AuthService)(nil)
|
|
||||||
|
|
||||||
// NewAuthService creates a user service object which can be passed into the context. The service
|
|
||||||
// requires a user repository which it will use to hit the database when needed.
|
|
||||||
func NewAuthService(userRepository domain.UserRepository) domainAuth.AuthService {
|
|
||||||
return &AuthService{userRepository: userRepository}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGoogleAuthUrl generates a URL which is used to redirect the user to the Google sign in page.
|
|
||||||
// This URL is in the Google domain and is not coupled with this application. The data from this URL
|
|
||||||
// will be passed into a callback and work to complete the Google OAuth workflow.
|
|
||||||
func (s *AuthService) GetGoogleAuthUrl() string {
|
|
||||||
url := auth.GoogleAuthConfig.AuthCodeURL(
|
|
||||||
"randomstate",
|
|
||||||
oauth2.AccessTypeOffline,
|
|
||||||
oauth2.ApprovalForce,
|
|
||||||
)
|
|
||||||
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
// GoogleAuthSuccess accepts the data from the Google login endpoint and uses it to fetch the users
|
|
||||||
// data. The data is then used to log the user in or create an account.
|
|
||||||
func (s *AuthService) GoogleAuthSuccess(state, code string) (domain.User, domain.GoogleUserInfo, error) {
|
|
||||||
// Ensure the state matches, prevents M.I.T.M. attacks
|
|
||||||
if state != "randomstate" {
|
|
||||||
return domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("States don't match, received %s", state)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get access token from Google
|
|
||||||
token, err := auth.GoogleAuthConfig.Exchange(context.Background(), code)
|
|
||||||
if err != nil {
|
|
||||||
return domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Code exchange failed: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the access token to get user data
|
|
||||||
googleUserInfo, err := auth.GetUserData(token.AccessToken)
|
|
||||||
if err != nil {
|
|
||||||
return domain.User{}, domain.GoogleUserInfo{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to get the user, user is nil when they don't exit
|
|
||||||
user, err := s.userRepository.GetGoogleUser(googleUserInfo.Id)
|
|
||||||
if err != nil {
|
|
||||||
return domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Failed to get db user: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A user was found
|
|
||||||
if user != nil {
|
|
||||||
return *user, googleUserInfo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// user did not exist, need to create one
|
|
||||||
newUser, err := s.userRepository.CreateGoogleUser(&googleUserInfo, token.RefreshToken)
|
|
||||||
if err != nil {
|
|
||||||
return domain.User{}, domain.GoogleUserInfo{}, fmt.Errorf("Repository failed to create user: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// temporary
|
|
||||||
return newUser, googleUserInfo, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
|
||||||
|
|
||||||
// UserService implements the domain.UserService defined in the domain module.
|
|
||||||
type UserService struct {
|
|
||||||
userRepository domain.UserRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time check to ensure the UserService implements domain.UserService
|
|
||||||
var _ domain.UserService = (*UserService)(nil)
|
|
||||||
|
|
||||||
// NewUserService creates a user service object which can be passed into the context. The service
|
|
||||||
// requires a user repository which it will use to hit the database when needed.
|
|
||||||
func NewUserService(userRepository domain.UserRepository) domain.UserService {
|
|
||||||
return &UserService{userRepository: userRepository}
|
|
||||||
}
|
|
||||||
0
internal/app/services/recipe_service.go
Normal file
0
internal/app/services/recipe_service.go
Normal file
0
internal/app/services/user_service.go
Normal file
0
internal/app/services/user_service.go
Normal file
@ -1,10 +0,0 @@
|
|||||||
package domain
|
|
||||||
|
|
||||||
import (
|
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AuthService interface {
|
|
||||||
GetGoogleAuthUrl() string
|
|
||||||
GoogleAuthSuccess(state, code string) (domain.User, domain.GoogleUserInfo, error)
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
package domain
|
|
||||||
|
|
||||||
import (
|
|
||||||
domainAuth "github.com/haydenhargreaves/Potion/internal/domain/auth"
|
|
||||||
domainUser "github.com/haydenhargreaves/Potion/internal/domain/user"
|
|
||||||
)
|
|
||||||
|
|
||||||
type InjectedDependencies struct {
|
|
||||||
UserService domainUser.UserService
|
|
||||||
AuthService domainAuth.AuthService
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package domain
|
|
||||||
|
|
||||||
type UserRepository interface {
|
|
||||||
CreateGoogleUser(googleUserInfo *GoogleUserInfo, googleRefreshToken string) (User, error)
|
|
||||||
GetGoogleUser(googleId string) (*User, error)
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
package domain
|
|
||||||
|
|
||||||
type UserService interface {
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
package domain
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// GoogleUserInfo is a data type which contains a mapping of the Google User Info API call.
|
|
||||||
type GoogleUserInfo struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
Verified bool `json:"verified_email"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
GivenName string `json:"given_name"`
|
|
||||||
FamilyName string `json:"family_name"`
|
|
||||||
Picture string `json:"picture"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// User is the database model of a user. There is no need to map to a different model so
|
|
||||||
// this will remain in the domain.
|
|
||||||
type User struct {
|
|
||||||
Id int
|
|
||||||
GoogleId string
|
|
||||||
Name string
|
|
||||||
Email string
|
|
||||||
ImageUrl string
|
|
||||||
GoogleRefreshToken string
|
|
||||||
Created time.Time
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"golang.org/x/oauth2/google"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GoogleAuthConfig is a global variable that stores the current state of the Google configuration.
|
|
||||||
// NewGoogleConfig should be called before this value is accessed, or the application will panic
|
|
||||||
// to prevent a null reference.
|
|
||||||
var GoogleAuthConfig oauth2.Config
|
|
||||||
|
|
||||||
// NewGOogleConfig creates a google authentication configuration. The configuration is set as a
|
|
||||||
// global variable which can be used by importing this module. The configuration is used to query
|
|
||||||
// the google OAuth API and collect user data.
|
|
||||||
//
|
|
||||||
// This function should be called before any calls to the Google API are made, otherwise a null
|
|
||||||
// value will be accessed and the call will panic.
|
|
||||||
//
|
|
||||||
// The global variable is returned by this function but is not needed.
|
|
||||||
func NewGoogleConfig(redirectUrl, clientId, clientSecret string, scope []string) oauth2.Config {
|
|
||||||
GoogleAuthConfig = oauth2.Config{
|
|
||||||
RedirectURL: redirectUrl,
|
|
||||||
ClientID: clientId,
|
|
||||||
ClientSecret: clientSecret,
|
|
||||||
Scopes: scope,
|
|
||||||
Endpoint: google.Endpoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
return GoogleAuthConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserData accepts a access token and calls the Google OAuth API to receive the respective user
|
|
||||||
// data. Any errors that occur will be wrapped and returned to the caller.
|
|
||||||
func GetUserData(accessToken string) (domain.GoogleUserInfo, error) {
|
|
||||||
googleApiUrl := fmt.Sprintf("https://www.googleapis.com/oauth2/v2/userinfo?access_token=%s", accessToken)
|
|
||||||
|
|
||||||
resp, err := http.Get(googleApiUrl)
|
|
||||||
if err != nil {
|
|
||||||
return domain.GoogleUserInfo{}, fmt.Errorf("Failed to fetch user info from Google: %s", err.Error())
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return domain.GoogleUserInfo{}, fmt.Errorf("Failed to read body from response: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
var googleUserInfo domain.GoogleUserInfo
|
|
||||||
if err := json.Unmarshal(body, &googleUserInfo); err != nil {
|
|
||||||
return domain.GoogleUserInfo{}, fmt.Errorf("Failed to parser response: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return googleUserInfo, nil
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com)
|
|
||||||
-- Desc: Create the users table in the database.
|
|
||||||
-- Date: 06/13/2025
|
|
||||||
|
|
||||||
BEGIN;
|
|
||||||
|
|
||||||
-- Create the users table
|
|
||||||
CREATE TABLE IF NOT EXISTS Users (
|
|
||||||
Id SERIAL PRIMARY KEY NOT NULL,
|
|
||||||
GoogleId TEXT UNIQUE NOT NULL,
|
|
||||||
Name VARCHAR(64) NOT NULL,
|
|
||||||
Email VARCHAR(128) UNIQUE NOT NULL,
|
|
||||||
ImageUrl TEXT,
|
|
||||||
GoogleRefreshToken TEXT,
|
|
||||||
Created TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserRepository struct {
|
|
||||||
db *sql.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile-time check to ensure the UserRepository implements domain.UserRepository
|
|
||||||
var _ domain.UserRepository = (*UserRepository)(nil)
|
|
||||||
|
|
||||||
// NewUserRepository creates a user repository object which is used by the user service to access
|
|
||||||
// the database. Any user related database operations will take place in this repository.
|
|
||||||
func NewUserRepository(db *sql.DB) domain.UserRepository {
|
|
||||||
return &UserRepository{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateGoogleUser creates a user entry in the users table. The refresh token is required along
|
|
||||||
// with the Google user info, in order to complete the database schema. Currently, Google login is
|
|
||||||
// the only support method of authentication, if this changes, this repository may need updates to
|
|
||||||
// match an updated table schema.
|
|
||||||
//
|
|
||||||
// This function will NOT check if the user already exists, if they do, it will return an error. For
|
|
||||||
// best results, pair this function with the GetGoogleUser which will return the user if it can find
|
|
||||||
// it.
|
|
||||||
func (r *UserRepository) CreateGoogleUser(googleUserInfo *domain.GoogleUserInfo, googleRefreshToken string) (domain.User, error) {
|
|
||||||
tx, err := r.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return domain.User{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var user domain.User
|
|
||||||
query := `INSERT INTO users
|
|
||||||
(GoogleId, Name, Email, ImageUrl, GoogleRefreshToken)
|
|
||||||
VALUES ($1, $2, $3, $4, $5) RETURNING *;`
|
|
||||||
|
|
||||||
if err := tx.QueryRow(
|
|
||||||
query,
|
|
||||||
googleUserInfo.Id,
|
|
||||||
googleUserInfo.Name,
|
|
||||||
googleUserInfo.Email,
|
|
||||||
googleUserInfo.Picture,
|
|
||||||
googleRefreshToken,
|
|
||||||
).Scan(
|
|
||||||
&user.Id,
|
|
||||||
&user.GoogleId,
|
|
||||||
&user.Name,
|
|
||||||
&user.Email,
|
|
||||||
&user.ImageUrl,
|
|
||||||
&user.GoogleRefreshToken,
|
|
||||||
&user.Created,
|
|
||||||
); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return domain.User{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return domain.User{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGoogleUser attempts to find a user in the database via its Google ID, not the database ID. This
|
|
||||||
// function is used when a user logs in with Google to prevent duplicate entries from being made. If
|
|
||||||
// no user is found, this function will return a null pointer but not an error.
|
|
||||||
func (r *UserRepository) GetGoogleUser(googleId string) (*domain.User, error) {
|
|
||||||
tx, err := r.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var user domain.User
|
|
||||||
query := `SELECT * FROM users WHERE GoogleId = $1`
|
|
||||||
|
|
||||||
if err := tx.QueryRow(query, googleId).Scan(
|
|
||||||
&user.Id,
|
|
||||||
&user.GoogleId,
|
|
||||||
&user.Name,
|
|
||||||
&user.Email,
|
|
||||||
&user.ImageUrl,
|
|
||||||
&user.GoogleRefreshToken,
|
|
||||||
&user.Created,
|
|
||||||
); err != nil {
|
|
||||||
// If no user was found, don't error, just return
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
tx.Rollback()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package templates
|
|
||||||
|
|
||||||
// AppLayout is the main application layout, this does not contain any content other than
|
|
||||||
// meta data, links, scripts and whatever is passed into it as a component.
|
|
||||||
templ AppLayout(title string, child templ.Component) {
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>{ title }</title>
|
|
||||||
<link rel="stylesheet" href="/v1/web/static/css/tailwind.css" />
|
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
@child
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
}
|
|
||||||
@ -1,63 +1,10 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
// templ: version: v0.3.865
|
// templ: version: v0.3.865
|
||||||
package templates
|
|
||||||
|
|
||||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
import "github.com/a-h/templ"
|
import "github.com/a-h/templ"
|
||||||
import templruntime "github.com/a-h/templ/runtime"
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
// AppLayout is the main application layout, this does not contain any content other than
|
|
||||||
// meta data, links, scripts and whatever is passed into it as a component.
|
|
||||||
func AppLayout(title string, child templ.Component) 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 = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var2 string
|
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/layouts/app_layout.templ`, Line: 12, Col: 16}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"stylesheet\" href=\"/v1/web/static/css/tailwind.css\"><script src=\"https://unpkg.com/htmx.org@2.0.4\"></script></head><body>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = child.Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</body></html>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
var _ = templruntime.GeneratedTemplate
|
||||||
0
internal/templates/pages/home.templ
Normal file
0
internal/templates/pages/home.templ
Normal file
10
internal/templates/pages/home_templ.go
Normal file
10
internal/templates/pages/home_templ.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.865
|
||||||
|
|
||||||
|
//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"
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
@ -1,45 +0,0 @@
|
|||||||
package templates
|
|
||||||
|
|
||||||
templ LoginPage() {
|
|
||||||
<div class="h-screen w-full grid place-items-center bg-gray-100">
|
|
||||||
<div class="w-3/4 sm:w-3/4 md:w-1/2 lg:w-2/7 bg-white border border-gray-200 rounded-xl shadow-2xs">
|
|
||||||
<div class="p-4 sm:p-7">
|
|
||||||
<div class="">
|
|
||||||
<h1 class="block text-2xl font-bold text-gray-800">
|
|
||||||
Sign in to Continue
|
|
||||||
</h1>
|
|
||||||
<p class="mt-2 text-sm text-gray-600">
|
|
||||||
You need to sign in to continue. Don't have an account? Signing in will
|
|
||||||
create one for you!
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="mt-5">
|
|
||||||
<a
|
|
||||||
href="/v1/api/auth/login"
|
|
||||||
class="w-full py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-2xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none"
|
|
||||||
>
|
|
||||||
<svg class="w-4 h-auto" width="46" height="47" viewBox="0 0 46 47" fill="none">
|
|
||||||
<path
|
|
||||||
d="M46 24.0287C46 22.09 45.8533 20.68 45.5013 19.2112H23.4694V27.9356H36.4069C36.1429 30.1094 34.7347 33.37 31.5957 35.5731L31.5663 35.8669L38.5191 41.2719L38.9885 41.3306C43.4477 37.2181 46 31.1669 46 24.0287Z"
|
|
||||||
fill="#4285F4"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
d="M23.4694 47C29.8061 47 35.1161 44.9144 39.0179 41.3012L31.625 35.5437C29.6301 36.9244 26.9898 37.8937 23.4987 37.8937C17.2793 37.8937 12.0281 33.7812 10.1505 28.1412L9.88649 28.1706L2.61097 33.7812L2.52296 34.0456C6.36608 41.7125 14.287 47 23.4694 47Z"
|
|
||||||
fill="#34A853"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
d="M10.1212 28.1413C9.62245 26.6725 9.32908 25.1156 9.32908 23.5C9.32908 21.8844 9.62245 20.3275 10.0918 18.8588V18.5356L2.75765 12.8369L2.52296 12.9544C0.909439 16.1269 0 19.7106 0 23.5C0 27.2894 0.909439 30.8731 2.49362 34.0456L10.1212 28.1413Z"
|
|
||||||
fill="#FBBC05"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
d="M23.4694 9.07688C27.8699 9.07688 30.8622 10.9863 32.5344 12.5725L39.1645 6.11C35.0867 2.32063 29.8061 0 23.4694 0C14.287 0 6.36607 5.2875 2.49362 12.9544L10.0918 18.8588C11.9987 13.1894 17.25 9.07688 23.4694 9.07688Z"
|
|
||||||
fill="#EB4335"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
Sign in with Google
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
// Code generated by templ - DO NOT EDIT.
|
|
||||||
|
|
||||||
// templ: version: v0.3.865
|
|
||||||
package templates
|
|
||||||
|
|
||||||
//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 LoginPage() 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 = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"h-screen w-full grid place-items-center bg-gray-100\"><div class=\"w-3/4 sm:w-3/4 md:w-1/2 lg:w-2/7 bg-white border border-gray-200 rounded-xl shadow-2xs\"><div class=\"p-4 sm:p-7\"><div class=\"\"><h1 class=\"block text-2xl font-bold text-gray-800\">Sign in to Continue</h1><p class=\"mt-2 text-sm text-gray-600\">You need to sign in to continue. Don't have an account? Signing in will create one for you!</p></div><div class=\"mt-5\"><a href=\"/v1/api/auth/login\" class=\"w-full py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-2xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none\"><svg class=\"w-4 h-auto\" width=\"46\" height=\"47\" viewBox=\"0 0 46 47\" fill=\"none\"><path d=\"M46 24.0287C46 22.09 45.8533 20.68 45.5013 19.2112H23.4694V27.9356H36.4069C36.1429 30.1094 34.7347 33.37 31.5957 35.5731L31.5663 35.8669L38.5191 41.2719L38.9885 41.3306C43.4477 37.2181 46 31.1669 46 24.0287Z\" fill=\"#4285F4\"></path> <path d=\"M23.4694 47C29.8061 47 35.1161 44.9144 39.0179 41.3012L31.625 35.5437C29.6301 36.9244 26.9898 37.8937 23.4987 37.8937C17.2793 37.8937 12.0281 33.7812 10.1505 28.1412L9.88649 28.1706L2.61097 33.7812L2.52296 34.0456C6.36608 41.7125 14.287 47 23.4694 47Z\" fill=\"#34A853\"></path> <path d=\"M10.1212 28.1413C9.62245 26.6725 9.32908 25.1156 9.32908 23.5C9.32908 21.8844 9.62245 20.3275 10.0918 18.8588V18.5356L2.75765 12.8369L2.52296 12.9544C0.909439 16.1269 0 19.7106 0 23.5C0 27.2894 0.909439 30.8731 2.49362 34.0456L10.1212 28.1413Z\" fill=\"#FBBC05\"></path> <path d=\"M23.4694 9.07688C27.8699 9.07688 30.8622 10.9863 32.5344 12.5725L39.1645 6.11C35.0867 2.32063 29.8061 0 23.4694 0C14.287 0 6.36607 5.2875 2.49362 12.9544L10.0918 18.8588C11.9987 13.1894 17.25 9.07688 23.4694 9.07688Z\" fill=\"#EB4335\"></path></svg> Sign in with Google</a></div></div></div></div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
|
||||||
@ -1,8 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: ["./**/*.html", "./**/*.templ", "./**/*.go",],
|
||||||
"./internal/templates/**/*.templ",
|
|
||||||
"./internal/templates/**/*.html",
|
|
||||||
],
|
|
||||||
theme: { extend: {}, },
|
theme: { extend: {}, },
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
@import "tailwindcss";
|
|
||||||
@source "./internal/templates/**/*.templ";
|
|
||||||
@plugin "daisyui";
|
|
||||||
0
web/static/css/style.css
Normal file
0
web/static/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user