Skip to content

Commit

Permalink
Added session based authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
KostLinux committed Jun 1, 2024
1 parent 0566fb2 commit d8db678
Show file tree
Hide file tree
Showing 19 changed files with 304 additions and 42 deletions.
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=postgres
POSTGRES_DB=postgres

# Status Page
STATUSPAGE_API_KEY=""
7 changes: 7 additions & 0 deletions bin/generate_api_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import os
import binascii

def generate_api_key():
return binascii.hexlify(os.urandom(16)).decode()

print(generate_api_key())
76 changes: 76 additions & 0 deletions controller/api/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package api

import (
"net/http"
"os"
"web/model"

"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/render"
)

type loginFormData struct {
Email string `form:"email"`
Username string `form:"username"`
Password string `form:"password"`
}

func Signup(c *gin.Context) {
var data loginFormData
if err := c.Bind(&data); err != nil {
c.Render(http.StatusBadRequest, render.Data{})
return
}

if !model.CheckUserExistance(data.Email) {
c.JSON(http.StatusBadRequest, gin.H{"error": "User already exists"})
return
}

user := model.CreateUser(data.Email, data.Username, data.Password)
if user == nil || user.ID == 0 {
c.Render(http.StatusBadRequest, render.Data{})
return
}

session := sessions.Default(c)
session.Set("userID", user.ID)
if err := session.Save(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save session"})
return
}
c.Redirect(http.StatusFound, os.Getenv("API_PATH")+"ping")
}

func Signin(c *gin.Context) {
var data loginFormData
if err := c.Bind(&data); err != nil {
c.Render(http.StatusBadRequest, render.Data{})
return
}

user := model.CheckPasswordMatch(data.Username, data.Password)
if user.ID == 0 {
c.Render(http.StatusUnauthorized, render.Data{})
return
}

session := sessions.Default(c)
session.Set("userID", user.ID)
if err := session.Save(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save session"})
return
}
c.Redirect(http.StatusFound, os.Getenv("API_PATH")+"ping")
}

func Signout(c *gin.Context) {
session := sessions.Default(c)
session.Clear()
if err := session.Save(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save session"})
return
}
c.Redirect(http.StatusFound, "/")
}
File renamed without changes.
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module web
go 1.22.1

require (
github.com/gin-contrib/sessions v1.0.1
github.com/jarcoal/httpmock v1.3.1
github.com/stretchr/testify v1.9.0
gorm.io/driver/postgres v1.5.7
Expand All @@ -15,13 +16,17 @@ require (
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.2.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
)

Expand All @@ -47,7 +52,7 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/crypto v0.23.0
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
Expand Down
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI=
github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-contrib/static v1.1.2 h1:c3kT4bFkUJn2aoRU3s6XnMjJT8J6nNWJkR0NglqmlZ4=
Expand All @@ -31,6 +33,14 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
Expand Down Expand Up @@ -73,6 +83,8 @@ github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
Expand Down
16 changes: 11 additions & 5 deletions helpers/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,28 @@ import (

func ServePageAssets(router *gin.Engine) {
if CheckFileExists("./public/index.html") {
router.Use(static.Serve("/assets", static.LocalFile("./public/assets", true)))
router.Use(static.Serve("/assets", static.LocalFile("./public/assets", false)))
} else {
router.Use(static.Serve("/assets", static.LocalFile("./views/assets", true)))
router.Use(static.Serve("/assets", static.LocalFile("./views/assets", false)))
assetsPerPage(router)
}
}

func assetsPerPage(router *gin.Engine) {
assetsDir, err := os.ReadDir("./views")
views, err := os.ReadDir("./views")
if err != nil {
log.Fatal(err)
}

for _, directory := range assetsDir {
for _, directory := range views {
if directory.IsDir() && strings.Contains(directory.Name(), "_page") {
router.Use(static.Serve("/"+directory.Name()+"/assets", static.LocalFile("./views/"+directory.Name()+"/assets", true)))
assetsPath := "./views/" + directory.Name() + "/assets"
if CheckFileNotExists(assetsPath) {
continue
}

// Serve page-specific assets
router.Use(static.Serve("/"+directory.Name()+"/assets", static.LocalFile(assetsPath, false)))
}
}
}
17 changes: 17 additions & 0 deletions helpers/users.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package helper

import (
"golang.org/x/crypto/bcrypt"
)

func EncryptPassword(password string) (string, error) {
encryptedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(encryptedPassword), nil
}

func CheckPasswordMatch(hashedPassword, password string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
9 changes: 8 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import (
"log"
"os"
"web/middleware"
"web/model"
db "web/repository/db"

"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memstore"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
_ "github.com/lib/pq"
Expand All @@ -20,11 +23,15 @@ func main() {
gin.SetMode(gin.ReleaseMode)
}

if err := db.NewDBConnection(); err != nil {
var err error
model.DB, err = db.NewDBConnection()
if err != nil {
log.Fatalf("failed to establish database connection: %v", err)
}

router := gin.New()
store := memstore.NewStore([]byte("secret"))
router.Use(sessions.Sessions("session", store))
httpRouter := middleware.NewRouter(router)

if err := httpRouter.Run(":" + os.Getenv("APP_PORT")); err != nil {
Expand Down
37 changes: 37 additions & 0 deletions middleware/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package middleware

import (
"net/http"
"os"
"web/model"

"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)

func Authentication() gin.HandlerFunc {
return func(c *gin.Context) {
// before request
session := sessions.Default(c)
sessionID := session.Get("userID")

if sessionID == nil {
// Check for API key in the request
apiKey := c.GetHeader("X-API-Key")
if apiKey != os.Getenv("STATUSPAGE_API_KEY") {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
} else {
userID := sessionID.(uint)
user, err := model.GetUserByID(userID)
if err != nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
c.Set("userID", user.ID)
}

c.Next()
}
}
13 changes: 7 additions & 6 deletions middleware/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,20 @@ func NewRouter(router *gin.Engine) *gin.Engine {

// HTML Template generated pages
router.GET("/status", httpTemplates.StatusPageResponse(), StatusPageMiddleware())
router.GET("/login", LoginPageMiddleware())

// Authentication
router.POST("/signup", api.Signup)
router.POST("/signin", api.Signin)
router.GET("/signout", api.Signout)

// API Handling
apiGroup := router.Group(os.Getenv("API_PATH"))
apiGroup := router.Group(os.Getenv("API_PATH"), Authentication())
{
apiGroup.GET("/ping", api.StatusOkPingResponse)
apiGroup.GET("/users", api.GetUsers)
}

// HTML Templates (e.g Status page)
router.LoadHTMLGlob("./views/**/*")
router.GET("/status", httpTemplates.StatusPageResponse(), StatusPageMiddleware())
router.GET("/login", LoginPageMiddleware())

// Error handling
router.NoRoute(errorController.StatusNotFound)

Expand Down
7 changes: 4 additions & 3 deletions migrations/20240403201102_init_tables.sql
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE users (
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO users (name) VALUES ('Alice');
INSERT INTO users (created_at, updated_at, email, username, password)
VALUES (NOW(), NOW(), 'statuspage@api.com', 'StatusPageAPI', 'b63729771dbc8166067f73c735c7a78dde61ccbe25d05d0cefec4ba2acbcfa23');
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DROP TABLE users;
-- +goose StatementEnd
-- +goose StatementEnd
Loading

0 comments on commit d8db678

Please sign in to comment.