B: getting closer to a login system

This commit is contained in:
Preston Baxter 2023-10-28 16:50:44 -05:00
parent ce0a301449
commit f7dc37fb02
16 changed files with 332 additions and 37 deletions

View File

@ -5,9 +5,9 @@ tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "rm **/*_templ.go && templ generate && go build -o ./tmp/main ."
cmd = "rm **/*_templ.go; templ generate --path ./templates && go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "dist"]
exclude_dir = ["assets", "tmp", "vendor", "testdata", "dist", "docker"]
exclude_file = []
exclude_regex = ["_test.go", "_templ.go"]
exclude_unchanged = false

1
ui/.gitignore vendored
View File

@ -1,3 +1,4 @@
dist/*
tmp/*
**/*_templ.go
docker/tmp/*

View File

@ -5,26 +5,23 @@ import (
)
type config struct {
Mongo *MongoConfig `json:"mogno"`
JwtSecret string `json:"jwt_secret"`
Env string `json:"env"`
Mongo *MongoConfig `mapstructure:"mongo"`
JwtSecret string `mapstructure:"jwt_secret"`
Env string `mapstructure:"env"`
}
type MongoConfig struct {
Host string `json:"host"`
Port string `json:"port"`
User string `json:"user"`
Password string `json:"password"`
EntDb string `json:"entity_db"`
EntCol string `json:"entity_col"`
Uri string `mapstructure:"uri"`
EntDb string `mapstructure:"ent_db"`
EntCol string `mapstructure:"ent_col"`
}
var cfg *config
func init() {
func Init() {
viper.SetConfigName("config") // name of config file (without extension)
viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name
viper.AddConfigPath("/etc/capstone/") // path to look for the config file in
viper.AddConfigPath("/etc/capstone") // path to look for the config file in
err := viper.ReadInConfig()
if err != nil {

160
ui/controllers/auth.go Normal file
View File

@ -0,0 +1,160 @@
package controllers
import (
"fmt"
"time"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/templates"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)
type LoginPostBody struct {
Email string `json:"email"`
Password string `json:"password"`
}
func SignUpHandler (c *gin.Context) {
//get uname and password.
conf := config.Config()
reqBody := &LoginPostBody{}
c.Request.ParseForm()
reqBody.Email = c.Request.FormValue("email")
reqBody.Password = c.Request.FormValue("password")
if reqBody.Email == "" {
renderTempl(c, templates.SignupPage("Please provide an email"))
return
}
if reqBody.Password == "" {
renderTempl(c, templates.SignupPage("Please provide a password"))
return
}
//Verify username and password
user, err := mongo.FindUserByEmail(reqBody.Email)
if err != nil {
renderTempl(c, templates.SignupPage("Error occured. Please try again later"))
return
}
if user != nil {
renderTempl(c, templates.SignupPage(fmt.Sprintf("user already exists for %s", reqBody.Email)))
return
}
user = &models.User{}
passHash, err := bcrypt.GenerateFromPassword([]byte(reqBody.Password), 10)
if err != nil {
renderTempl(c, templates.SignupPage("Signup failed. Please try again later"))
return
}
user.PassowrdHash = string(passHash)
user.Email = reqBody.Email
err = mongo.SaveModel(user)
if err != nil {
renderTempl(c, templates.SignupPage("Signup failed. Please try again later"))
return
}
//build jwt
token := jwt.NewWithClaims(jwt.SigningMethodHS256,
jwt.MapClaims{
"sub": user.UserId,
"exp": time.Now().Add(12 * time.Hour).Unix(),
},
)
jwtStr, err := token.SignedString(conf.JwtSecret)
if err != nil {
renderTempl(c, templates.SignupPage("Signup failed. Please try again later"))
return
}
//store jwt as cookie
//TODO: Make sure set secure for prd deployment
c.SetCookie("authorization", jwtStr, 3600 * 24, "", "", false, true)
c.Redirect(302, "/dashboard")
}
func LoginHandler(c *gin.Context) {
//get uname and password.
conf := config.Config()
reqBody := &LoginPostBody{}
c.Request.ParseForm()
reqBody.Email = c.Request.FormValue("email")
reqBody.Password = c.Request.FormValue("password")
if reqBody.Email == "" {
renderTempl(c, templates.LoginPage("Please provide an email"))
return
}
if reqBody.Password == "" {
renderTempl(c, templates.LoginPage("Please provide a password"))
return
}
//Verify username and password
user, err := mongo.FindUserByEmail(reqBody.Email)
if err != nil {
renderTempl(c, templates.LoginPage(err.Error()))
return
}
if user == nil {
renderTempl(c, templates.LoginPage(fmt.Sprintf("No user found for %s", reqBody.Email)))
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PassowrdHash), []byte(reqBody.Password)); err != nil {
renderTempl(c, templates.LoginPage("Email and password are incorrect"))
return
}
//build jwt
token := jwt.NewWithClaims(jwt.SigningMethodHS256,
jwt.MapClaims{
"sub": user.UserId,
"exp": time.Now().Add(12 * time.Hour).Unix(),
},
)
jwtStr, err := token.SignedString(conf.JwtSecret)
if err != nil {
renderTempl(c, templates.LoginPage("An error occured. Please try again later"))
}
//store jwt as cookie
var secure bool
if conf.Env == "dev" {
secure = false
} else {
secure = true
}
c.SetCookie("authorization", jwtStr, 3600 * 24, "", "", secure, true)
c.Redirect(302, "/dashboard")
}
func LogoutHandler(c *gin.Context) {
conf := config.Config()
var secure bool
if conf.Env == "dev" {
secure = false
} else {
secure = true
}
c.SetCookie("authorization", "", 3600 * 24, "", "", secure, true)
c.Redirect(302, "/login")
}

View File

@ -1,21 +1,37 @@
package controllers
import (
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/middleware"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/templates"
"github.com/gin-gonic/gin"
)
var mongo *db.DB
var log *logrus.Logger
func BuildRouter(r *gin.Engine) {
conf := config.Config()
var err error
mongo, err = db.NewClient(conf.Mongo.Uri)
if err != nil {
panic(err)
}
r.GET("/", middleware.AuthMiddleware(false) ,LandingPage)
r.GET("/login", middleware.AuthMiddleware(false), LoginPage)
r.GET("/signup", middleware.AuthMiddleware(false), SignUpPage)
r.POST("/login", LoginHandler)
r.POST("/signup", SignUpHandler)
r.POST("/logout", LogoutHandler)
dashboard := r.Group("/dashboard")
dashboard.Use(middleware.AuthMiddleware(true))
dashboard.GET("/", DashboardPage)
}
func LandingPage(c *gin.Context) {
renderTempl(c, templates.LandingPage(false))
}
func LoginPage(c *gin.Context) {
renderTempl(c, templates.LoginPage())
}

22
ui/controllers/pages.go Normal file
View File

@ -0,0 +1,22 @@
package controllers
import (
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/templates"
"github.com/gin-gonic/gin"
)
func LandingPage(c *gin.Context) {
renderTempl(c, templates.LandingPage(false))
}
func LoginPage(c *gin.Context) {
renderTempl(c, templates.LoginPage(""))
}
func SignUpPage(c *gin.Context) {
renderTempl(c, templates.SignupPage(""))
}
func DashboardPage(c *gin.Context) {
c.JSON(200, gin.H{"response": "dashboard"})
}

View File

@ -14,3 +14,20 @@ func renderTempl(c *gin.Context, tmpl templ.Component) {
c.Data(200, "text/html", buf.Bytes())
}
func badRequest(c *gin.Context, reason string) {
c.JSON(400, map[string]string{"error": reason})
}
func serverError(c *gin.Context, reason string) {
c.JSON(504, map[string]string{"error": reason})
}
func notFound(c *gin.Context, reason string) {
c.JSON(404, map[string]string{"error": reason})
}
func unauthorized(c *gin.Context, reason string) {
c.JSON(403, map[string]string{"error": reason})
}

View File

@ -24,3 +24,7 @@ func NewClient(uri string) (*DB, error) {
return &DB{client: client}, nil
}
func (db *DB) SaveModel(m Model) error {
return m.Save(db.client)
}

View File

@ -28,9 +28,11 @@ func (user *User) Save(client *mongo.Client) error {
if user.mongoId.IsZero() {
now := time.Now()
user.EntityType = USER_TYPE
user.CreatedAt = now
user.UpdatedAt = now
user.model = &model{
EntityType: USER_TYPE,
CreatedAt: now,
UpdatedAt: now,
}
user.UserId = uuid.New().String()
user.mongoId = primitive.NewObjectIDFromTimestamp(now)
}

View File

@ -6,6 +6,7 @@ import (
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/db/models"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
@ -16,6 +17,9 @@ func (db *DB) FindUserByEmail(email string) (*models.User, error) {
res := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).FindOne(context.Background(), bson.M{"email": email}, opts)
if res.Err() != nil {
if res.Err() == mongo.ErrNoDocuments {
return nil, nil
}
return nil, res.Err()
}
@ -35,6 +39,9 @@ func (db *DB) FindUserById(id string) (*models.User, error) {
res := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).FindOne(context.Background(), bson.M{"user_id": id}, opts)
if res.Err() != nil {
if res.Err() == mongo.ErrNoDocuments {
return nil, nil
}
return nil, res.Err()
}
@ -54,6 +61,9 @@ func (db *DB) FindAllUsers() ([]models.User, error) {
res, err := db.client.Database(conf.Mongo.EntDb).Collection(conf.Mongo.EntCol).Find(context.Background(), bson.M{"ent": models.USER_TYPE}, opts)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, nil
}
return nil, res.Err()
}

View File

@ -0,0 +1,8 @@
version: '3.8'
services:
mongodb:
image: mongo:7
ports:
- '27017:27017'
volumes:
- ./tmp/mongo:/data/db

View File

@ -5,8 +5,11 @@ go 1.19
require (
github.com/a-h/templ v0.2.408
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/google/uuid v1.1.2
github.com/spf13/viper v1.17.0
go.mongodb.org/mongo-driver v1.12.1
golang.org/x/crypto v0.13.0
)
require (
@ -34,6 +37,7 @@ require (
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
@ -48,11 +52,10 @@ require (
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect

View File

@ -84,6 +84,8 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -140,6 +142,7 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
@ -195,6 +198,8 @@ github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9c
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
@ -393,11 +398,14 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -1,11 +1,13 @@
package main
import (
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/config"
"git.preston-baxter.com/Preston_PLB/capstone/frontend-service/controllers"
"github.com/gin-gonic/gin"
)
func main() {
config.Init()
r := gin.Default()
controllers.BuildRouter(r)

View File

@ -1,6 +1,6 @@
package templates
templ LoginContent() {
templ LoginContent(signup bool, errorMsg string) {
<main>
<section class="absolute w-full h-full">
<div
@ -14,10 +14,27 @@ templ LoginContent() {
class="relative flex flex-col min-w-0 break-words w-full mb-6 shadow-lg rounded-lg bg-gray-300 border-0"
>
<div class="flex-auto px-4 lg:px-10 py-10 pt-10">
if errorMsg != "" {
<div role="alert">
<div class="bg-red-500 text-white font-bold rounded-t px-4 py-2">
Error
</div>
<div class="border border-t-0 border-red-400 rounded-b bg-red-100 px-4 py-3 text-red-700">
<p>{ errorMsg }</p>
</div>
</div>
}
<div class="text-gray-500 text-center mb-3 font-bold">
<small>Sign in</small>
</div>
<form>
<form
if signup {
action="/signup"
} else {
action="/login"
}
method="POST"
>
<div class="relative w-full mb-3">
<label
class="block uppercase text-gray-700 text-xs font-bold mb-2"
@ -27,6 +44,7 @@ templ LoginContent() {
class="border-0 px-3 py-3 placeholder-gray-400 text-gray-700 bg-white rounded text-sm shadow focus:outline-none focus:ring w-full"
placeholder="Email"
style="transition: all 0.15s ease 0s;"
name="email"
/>
</div>
<div class="relative w-full mb-3">
@ -38,6 +56,7 @@ templ LoginContent() {
class="border-0 px-3 py-3 placeholder-gray-400 text-gray-700 bg-white rounded text-sm shadow focus:outline-none focus:ring w-full"
placeholder="Password"
style="transition: all 0.15s ease 0s;"
name="password"
/>
</div>
<div>
@ -52,22 +71,35 @@ templ LoginContent() {
</div>
<div class="text-center mt-6">
<button
type="submit"
class="bg-gray-900 text-white active:bg-gray-700 text-sm font-bold uppercase px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1 w-full"
type="button"
style="transition: all 0.15s ease 0s;"
>
Sign In
if signup {
{ "Signup" }
} else {
{ "Login" }
}
</button>
</div>
</form>
</div>
</div>
<div class="flex flex-wrap mt-6">
<div class="w-1/2">
<a href="#pablo" class="text-gray-300"><small>Forgot password?</small></a>
</div>
<div class="w-1/2 text-right">
<a href="#pablo" class="text-gray-300"><small>Create new account</small></a>
<div class="flex flex-wrap mt-2 py-5">
if !signup {
<div class="w-1/2 text-center">
<a href="#pablo" class="text-gray-800"><small>Forgot password?</small></a>
</div>
}
if signup {
<div class="w-full text-center">
<a href="/login" class="text-gray-800"><small>Log in instead</small></a>
</div>
} else {
<div class="w-1/2 text-center">
<a href="/signup" class="text-gray-800"><small>Create an account</small></a>
</div>
}
</div>
</div>
</div>

View File

@ -1,12 +1,25 @@
package templates
templ LoginPage() {
templ LoginPage(errorMsg string) {
<!DOCTYPE html>
<html>
@Head()
<body class="text-gray-800 antialiased">
@Nav(false)
@LoginContent()
@LoginContent(false, errorMsg)
</body>
@Footer()
@toggleNavBar()
</html>
}
templ SignupPage(errorMsg string) {
<!DOCTYPE html>
<html>
@Head()
<body class="text-gray-800 antialiased">
@Nav(false)
@LoginContent(true, errorMsg)
</body>
@Footer()
@toggleNavBar()