added login and test admin page
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
api\ tests.txt
|
||||
ccsecrets
|
||||
ccsecrets.db
|
||||
ccsecrets.db
|
||||
.env
|
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"ccsecrets/models"
|
||||
"ccsecrets/utils/token"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -17,6 +18,11 @@ type RegisterInput struct {
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type LoginInput struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
func Register(c *gin.Context) {
|
||||
var input RegisterInput
|
||||
|
||||
@@ -36,7 +42,7 @@ func Register(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"Error hashing password": err.Error()})
|
||||
return
|
||||
} else {
|
||||
fmt.Printf("Hashed password value is '%s'\n", string(hashedPassword))
|
||||
fmt.Printf("Register generated hashed password value '%s' from '%s'\n", string(hashedPassword), input.Password)
|
||||
}
|
||||
u.Password = string(hashedPassword)
|
||||
|
||||
@@ -52,3 +58,49 @@ func Register(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "registration success"})
|
||||
}
|
||||
|
||||
func Login(c *gin.Context) {
|
||||
|
||||
var input LoginInput
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
u := models.User{}
|
||||
|
||||
u.UserName = input.Username
|
||||
u.Password = input.Password
|
||||
|
||||
fmt.Printf("Login checking username '%s' and password '%s'\n", u.UserName, u.Password)
|
||||
|
||||
token, err := models.LoginCheck(u.UserName, u.Password)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "username or password is incorrect."})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"token": token})
|
||||
|
||||
}
|
||||
|
||||
func CurrentUser(c *gin.Context) {
|
||||
|
||||
user_id, err := token.ExtractTokenID(c)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
u, err := models.GetUserByID(user_id)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "success", "data": u})
|
||||
}
|
||||
|
2
go.mod
2
go.mod
@@ -3,8 +3,10 @@ module ccsecrets
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/gin-gonic/gin v1.9.0
|
||||
github.com/jmoiron/sqlx v1.3.5
|
||||
github.com/joho/godotenv v1.5.1
|
||||
golang.org/x/crypto v0.7.0
|
||||
modernc.org/sqlite v1.21.0
|
||||
)
|
||||
|
4
go.sum
4
go.sum
@@ -7,6 +7,8 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583j
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
@@ -33,6 +35,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
|
8
main.go
8
main.go
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"ccsecrets/controllers"
|
||||
"ccsecrets/middlewares"
|
||||
"ccsecrets/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -9,13 +10,20 @@ import (
|
||||
|
||||
func main() {
|
||||
|
||||
// Initiate connection to sqlite and make sure our schema is up to date
|
||||
models.ConnectDatabase()
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
public := r.Group("/api")
|
||||
|
||||
// Define our routes underneath /api
|
||||
public.POST("/register", controllers.Register)
|
||||
public.POST("/login", controllers.Login)
|
||||
|
||||
protected := r.Group("/api/admin")
|
||||
protected.Use(middlewares.JwtAuthMiddleware())
|
||||
protected.GET("/user", controllers.CurrentUser)
|
||||
|
||||
r.Run(":8080")
|
||||
|
||||
|
21
middlewares/middlewares.go
Normal file
21
middlewares/middlewares.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"ccsecrets/utils/token"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func JwtAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
err := token.TokenValid(c)
|
||||
if err != nil {
|
||||
c.String(http.StatusUnauthorized, "Unauthorized")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
@@ -3,12 +3,14 @@ package models
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"ccsecrets/utils"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/joho/godotenv"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
@@ -59,6 +61,13 @@ const createSchema string = `
|
||||
func ConnectDatabase() {
|
||||
var err error
|
||||
|
||||
// Load data from environment file
|
||||
err = godotenv.Load(".env")
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error loading .env file")
|
||||
}
|
||||
|
||||
// Try using sqlite as our database
|
||||
sqlPath := utils.GetFilePath(sqlFile)
|
||||
db, err = sqlx.Open("sqlite", sqlPath)
|
||||
@@ -84,15 +93,19 @@ func DisconnectDatabase() {
|
||||
|
||||
func CreateTables() {
|
||||
var err error
|
||||
var rowCount int
|
||||
// Create database tables if it doesn't exist
|
||||
// Roles table should go first since other tables refer to it
|
||||
if _, err = db.Exec(createRoles); err != nil {
|
||||
fmt.Printf("Error checking roles table : '%s'", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, err = db.Exec("INSERT INTO roles VALUES(1, 'Admin', false, true);"); err != nil {
|
||||
fmt.Printf("Error adding initial admin role : '%s'", err)
|
||||
os.Exit(1)
|
||||
rowCount, _ = CheckCount("roles")
|
||||
if rowCount == 0 {
|
||||
if _, err = db.Exec("INSERT INTO roles VALUES(1, 'Admin', false, true);"); err != nil {
|
||||
fmt.Printf("Error adding initial admin role : '%s'", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Users table
|
||||
@@ -100,9 +113,12 @@ func CreateTables() {
|
||||
fmt.Printf("Error checking users table : '%s'", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, err = db.Exec("INSERT INTO users VALUES(1, 1, 'Administrator', 'password', 'token');"); err != nil {
|
||||
fmt.Printf("Error adding initial admin role : '%s'", err)
|
||||
os.Exit(1)
|
||||
rowCount, _ = CheckCount("users")
|
||||
if rowCount == 0 {
|
||||
if _, err = db.Exec("INSERT INTO users VALUES(1, 1, 'Administrator', 'password', 'token');"); err != nil {
|
||||
fmt.Printf("Error adding initial admin role : '%s'", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
// Secrets table
|
||||
if _, err = db.Exec(createSecrets); err != nil {
|
||||
@@ -123,6 +139,24 @@ func CreateTables() {
|
||||
}
|
||||
}
|
||||
|
||||
// Count the number of records in the sqlite database
|
||||
// Borrowed from https://gist.github.com/trkrameshkumar/f4f1c00ef5d578561c96?permalink_comment_id=2687592#gistcomment-2687592
|
||||
func CheckCount(tablename string) (int, error) {
|
||||
var count int
|
||||
stmt, err := db.Prepare("SELECT COUNT(*) as count FROM " + tablename)
|
||||
if err != nil {
|
||||
fmt.Printf("CheckCount error preparing sqlite statement : '%s'\n", err)
|
||||
return 0, err
|
||||
}
|
||||
err = stmt.QueryRow().Scan(&count)
|
||||
if err != nil {
|
||||
fmt.Printf("CheckCount error querying database record count : '%s'\n", err)
|
||||
return 0, err
|
||||
}
|
||||
stmt.Close() // or use defer rows.Close(), idc
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// From https://stackoverflow.com/a/60100045
|
||||
func GenerateInsertMethod(q interface{}) (string, error) {
|
||||
if reflect.ValueOf(q).Kind() == reflect.Struct {
|
||||
|
@@ -1,6 +1,12 @@
|
||||
package models
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"ccsecrets/utils/token"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
UserId int `db:"UserId"`
|
||||
@@ -27,3 +33,69 @@ func (u *User) SaveUser() (*User, error) {
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func VerifyPassword(password, hashedPassword string) error {
|
||||
fmt.Printf("VerifyPassword comparing password vs hashed:\n'%s'\n'%s'\n", password, hashedPassword)
|
||||
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
}
|
||||
|
||||
func LoginCheck(username string, password string) (string, error) {
|
||||
|
||||
var err error
|
||||
|
||||
u := User{}
|
||||
|
||||
// Query database for matching user object
|
||||
err = db.QueryRowx("SELECT * FROM Users WHERE Username=?", username).StructScan(&u)
|
||||
|
||||
fmt.Printf("LoginCheck retrieved user '%v' from database\n", u)
|
||||
|
||||
//err = DB.Model(User{}).Where("username = ?", username).Take(&u).Error
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = VerifyPassword(password, u.Password)
|
||||
|
||||
if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
fmt.Printf("LoginCheck says password doesn't match stored hash.\n")
|
||||
return "", err
|
||||
} else {
|
||||
fmt.Printf("LoginCheck verified password against stored hash.\n")
|
||||
}
|
||||
|
||||
token, err := token.GenerateToken(uint(u.UserId))
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("LoginCheck error generating token : '%s'\n", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
|
||||
}
|
||||
|
||||
func GetUserByID(uid uint) (User, error) {
|
||||
|
||||
var u User
|
||||
|
||||
// Query database for matching user object
|
||||
err := db.QueryRowx("SELECT * FROM Users WHERE UserId=?", uid).StructScan(&u)
|
||||
if err != nil {
|
||||
return u, errors.New("user not found")
|
||||
}
|
||||
/*
|
||||
if err := DB.First(&u, uid).Error; err != nil {
|
||||
return u, errors.New("User not found!")
|
||||
}
|
||||
*/
|
||||
u.PrepareGive()
|
||||
|
||||
return u, nil
|
||||
|
||||
}
|
||||
|
||||
func (u *User) PrepareGive() {
|
||||
u.Password = ""
|
||||
}
|
||||
|
79
utils/token/token.go
Normal file
79
utils/token/token.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func GenerateToken(user_id uint) (string, error) {
|
||||
|
||||
token_lifespan, err := strconv.Atoi(os.Getenv("TOKEN_HOUR_LIFESPAN"))
|
||||
if err != nil {
|
||||
fmt.Printf("GenerateToken Error getting env value TOKEN_HOUR_LIFESPAN\n")
|
||||
return "", err
|
||||
}
|
||||
|
||||
claims := jwt.MapClaims{}
|
||||
claims["authorized"] = true
|
||||
claims["user_id"] = user_id
|
||||
claims["exp"] = time.Now().Add(time.Hour * time.Duration(token_lifespan)).Unix()
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
return token.SignedString([]byte(os.Getenv("API_SECRET")))
|
||||
|
||||
}
|
||||
|
||||
func TokenValid(c *gin.Context) error {
|
||||
tokenString := ExtractToken(c)
|
||||
_, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(os.Getenv("API_SECRET")), nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExtractToken(c *gin.Context) string {
|
||||
token := c.Query("token")
|
||||
if token != "" {
|
||||
return token
|
||||
}
|
||||
bearerToken := c.Request.Header.Get("Authorization")
|
||||
if len(strings.Split(bearerToken, " ")) == 2 {
|
||||
return strings.Split(bearerToken, " ")[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func ExtractTokenID(c *gin.Context) (uint, error) {
|
||||
|
||||
tokenString := ExtractToken(c)
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(os.Getenv("API_SECRET")), nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if ok && token.Valid {
|
||||
uid, err := strconv.ParseUint(fmt.Sprintf("%.0f", claims["user_id"]), 10, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint(uid), nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
Reference in New Issue
Block a user