6 Commits
Author SHA1 Message Date
phoenixandphoenix 2da394c7a4 update go (#42)
Reviewed-on: phoenix/textsender-auth#42
Co-authored-by: phoenix <mail@kundeng.us>
Co-committed-by: phoenix <mail@kundeng.us>
2026-05-03 17:07:10 -04:00
phoenixandphoenix f556d729a5 Update go (#40)
Reviewed-on: phoenix/textsender-auth#40
Co-authored-by: phoenix <mail@kundeng.us>
Co-committed-by: phoenix <mail@kundeng.us>
2026-04-04 22:09:10 -04:00
phoenixandphoenix e06a54947f tsk-6: Populate IssuedAt for LoginResult (#39)
Closes #36

Reviewed-on: phoenix/textsender-auth#39
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2026-01-04 02:00:39 +00:00
phoenixandphoenix 689762c72a tsk-35: Update name of user (#38)
Closes #35

Reviewed-on: phoenix/textsender-auth#38
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2026-01-01 22:41:08 +00:00
phoenixandphoenix de171b790d tsk-34: Update last login of user (#37)
Closes #34

Reviewed-on: phoenix/textsender-auth#37
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-12-31 23:13:02 +00:00
phoenixandphoenix d34cad033d tsk-30: Update password (#33)
Closes #30

Reviewed-on: phoenix/textsender-auth#33
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-12-26 21:14:40 +00:00
29 changed files with 1339 additions and 202 deletions
+3 -3
View File
@@ -16,7 +16,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.25.4' # You can specify a specific version or 'stable'
go-version: '1.26.2'
- name: Build
run: |
@@ -49,7 +49,7 @@ jobs:
runs-on: ubuntu-24.04
services:
postgres:
image: postgres:18.0
image: postgres:18.3-alpine
env:
POSTGRES_USER: ${{ secrets.DB_TEST_USER }}
POSTGRES_PASSWORD: ${{ secrets.DB_TEST_PASSWORD }}
@@ -68,7 +68,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: '1.25.4'
go-version: '1.26.2'
- name: Install PostgreSQL client
run: sudo apt update && sudo apt-get install -y postgresql-client
+1 -1
View File
@@ -1,5 +1,5 @@
# Multi-stage Dockerfile for Go application
FROM golang:1.25.4 AS builder
FROM golang:1.26.2 AS builder
WORKDIR /app
-1
View File
@@ -23,7 +23,6 @@ make build
Generate API documentation
```
go install github.com/swaggo/swag/cmd/swag@latest
go get -u github.com/swaggo/http-swagger/v2
swag init --generalInfo main.go --dir ./cmd/api,./internal/handler --output docs/ --parseDependency --parseInternal
```
+3
View File
@@ -21,6 +21,7 @@ import (
"git.kundeng.us/phoenix/textsender-auth/internal/handler"
"git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint"
mdleware "git.kundeng.us/phoenix/textsender-auth/internal/middleware"
"git.kundeng.us/phoenix/textsender-auth/internal/services"
"git.kundeng.us/phoenix/textsender-auth/internal/store"
)
@@ -82,6 +83,7 @@ func main() {
refreshHandler := handler.NewRefreshHandler(cfg, userStore, serviceStore)
router := chi.NewRouter()
jwtService := services.NewJWTService(config.GetSecretKey())
// Configure CORS
router.Use(cors.Handler(cors.Options{
@@ -102,6 +104,7 @@ func main() {
router.Method("Post", endpoint.CreateServiceUser, http.HandlerFunc(serviceHandler.Register))
router.Method("Post", endpoint.LoginServiceUser, http.HandlerFunc(serviceHandler.Login))
router.Method("Post", endpoint.TokenRefresh, http.HandlerFunc(refreshHandler.Refresh))
router.Method("PATCH", endpoint.UpdatePassword, mdleware.AuthMiddleware(jwtService)(http.HandlerFunc(loginHandler.UpdatePassword)))
router.Method("GET", "/swagger/*", httpSwagger.Handler(
httpSwagger.URL(fmt.Sprintf("http://localhost:%s/swagger/doc.json", config.Port)),
+13 -8
View File
@@ -4,21 +4,24 @@ import (
"context"
"flag"
"fmt"
"net/http"
"os"
"path"
"testing"
"github.com/gorilla/mux"
"github.com/go-chi/chi/v5"
"github.com/joho/godotenv"
"git.kundeng.us/phoenix/textsender-auth/internal/config"
"git.kundeng.us/phoenix/textsender-auth/internal/db"
"git.kundeng.us/phoenix/textsender-auth/internal/handler"
"git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint"
mdleware "git.kundeng.us/phoenix/textsender-auth/internal/middleware"
"git.kundeng.us/phoenix/textsender-auth/internal/services"
"git.kundeng.us/phoenix/textsender-auth/internal/store"
)
var testRouter *mux.Router
var testRouter *chi.Mux
func TestMain(m *testing.M) {
cfg := load()
@@ -44,12 +47,14 @@ func TestMain(m *testing.M) {
serviceHandler := handler.NewServiceHandler(cfg, serviceStore)
refreshHandler := handler.NewRefreshHandler(cfg, userStore, serviceStore)
testRouter = mux.NewRouter()
testRouter.HandleFunc(endpoint.Register, userHandler.Register).Methods("POST")
testRouter.HandleFunc(endpoint.Login, loginHandler.Login).Methods("POST")
testRouter.HandleFunc(endpoint.CreateServiceUser, serviceHandler.Register).Methods("POST")
testRouter.HandleFunc(endpoint.LoginServiceUser, serviceHandler.Login).Methods("POST")
testRouter.HandleFunc(endpoint.TokenRefresh, refreshHandler.Refresh).Methods("POST")
testRouter = chi.NewRouter()
jwtService := services.NewJWTService(config.GetSecretKey())
testRouter.Method("POST", endpoint.Register, http.HandlerFunc(userHandler.Register))
testRouter.Method("POST", endpoint.Login, http.HandlerFunc(loginHandler.Login))
testRouter.Method("POST", endpoint.CreateServiceUser, http.HandlerFunc(serviceHandler.Register))
testRouter.Method("POST", endpoint.LoginServiceUser, http.HandlerFunc(serviceHandler.Login))
testRouter.Method("POST", endpoint.TokenRefresh, http.HandlerFunc(refreshHandler.Refresh))
testRouter.Method("PATCH", endpoint.UpdatePassword, mdleware.AuthMiddleware(jwtService)(http.HandlerFunc(loginHandler.UpdatePassword)))
code := m.Run()
os.Exit(code)
+1 -1
View File
@@ -18,7 +18,7 @@ services:
# PostgreSQL Database Service
auth_db:
image: postgres:18.0-alpine # Use an official Postgres image (Alpine variant is smaller)
image: postgres:18.3-alpine # Use an official Postgres image (Alpine variant is smaller)
container_name: textsender_auth_db # Optional: Give the container a specific name
environment:
# These MUST match the user, password, and database name in the DATABASE_URL above
+207 -1
View File
@@ -275,9 +275,146 @@ const docTemplate = `{
}
}
}
},
"/user/name/update": {
"patch": {
"security": [
{
"BearerAuth": []
}
],
"description": "Update the first or last name of a user (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Update name of user",
"parameters": [
{
"description": "Data to update name of user",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.UpdateNameRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.UpdateNameResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.UpdateNameResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handler.UpdateNameResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.UpdateNameResponse"
}
}
}
}
},
"/user/password/update": {
"patch": {
"security": [
{
"BearerAuth": []
}
],
"description": "Update the password of a regular account (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Update Password",
"parameters": [
{
"description": "Needed data to update password",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordResponse"
}
}
}
}
}
},
"definitions": {
"git_kundeng_us_phoenix_textsender-models_tx0_user.User": {
"type": "object",
"properties": {
"created": {
"type": "string"
},
"first_name": {
"type": "string"
},
"id": {
"type": "string"
},
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
"password": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"handler.LoginAccount": {
"type": "object",
"properties": {
@@ -417,6 +554,65 @@ const docTemplate = `{
}
}
},
"handler.UpdateNameRequest": {
"type": "object",
"properties": {
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.UpdateNameResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/git_kundeng_us_phoenix_textsender-models_tx0_user.User"
}
},
"message": {
"type": "string"
}
}
},
"handler.UpdatePasswordRequest": {
"type": "object",
"properties": {
"confirmed_password": {
"type": "string"
},
"current_password": {
"type": "string"
},
"updated_password": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.UpdatePasswordResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/git_kundeng_us_phoenix_textsender-models_tx0_user.User"
}
},
"message": {
"type": "string"
}
}
},
"token.Login": {
"type": "object",
"properties": {
@@ -426,24 +622,34 @@ const docTemplate = `{
"expires_in": {
"type": "integer"
},
"issued_at": {
"type": "integer"
},
"token_type": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"user.ServiceUser": {
"type": "object",
"properties": {
"date_created": {
"created": {
"type": "string"
},
"id": {
"type": "string"
},
"last_login": {
"type": "string"
},
"passphrase": {
"type": "string"
},
"username": {
"description": "TODO: Remove the omitempty tags at a later point",
"type": "string"
}
}
+207 -1
View File
@@ -269,9 +269,146 @@
}
}
}
},
"/user/name/update": {
"patch": {
"security": [
{
"BearerAuth": []
}
],
"description": "Update the first or last name of a user (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Update name of user",
"parameters": [
{
"description": "Data to update name of user",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.UpdateNameRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.UpdateNameResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.UpdateNameResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handler.UpdateNameResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.UpdateNameResponse"
}
}
}
}
},
"/user/password/update": {
"patch": {
"security": [
{
"BearerAuth": []
}
],
"description": "Update the password of a regular account (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"users"
],
"summary": "Update Password",
"parameters": [
{
"description": "Needed data to update password",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.UpdatePasswordResponse"
}
}
}
}
}
},
"definitions": {
"git_kundeng_us_phoenix_textsender-models_tx0_user.User": {
"type": "object",
"properties": {
"created": {
"type": "string"
},
"first_name": {
"type": "string"
},
"id": {
"type": "string"
},
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
"password": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"handler.LoginAccount": {
"type": "object",
"properties": {
@@ -411,6 +548,65 @@
}
}
},
"handler.UpdateNameRequest": {
"type": "object",
"properties": {
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.UpdateNameResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/git_kundeng_us_phoenix_textsender-models_tx0_user.User"
}
},
"message": {
"type": "string"
}
}
},
"handler.UpdatePasswordRequest": {
"type": "object",
"properties": {
"confirmed_password": {
"type": "string"
},
"current_password": {
"type": "string"
},
"updated_password": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.UpdatePasswordResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/git_kundeng_us_phoenix_textsender-models_tx0_user.User"
}
},
"message": {
"type": "string"
}
}
},
"token.Login": {
"type": "object",
"properties": {
@@ -420,24 +616,34 @@
"expires_in": {
"type": "integer"
},
"issued_at": {
"type": "integer"
},
"token_type": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"user.ServiceUser": {
"type": "object",
"properties": {
"date_created": {
"created": {
"type": "string"
},
"id": {
"type": "string"
},
"last_login": {
"type": "string"
},
"passphrase": {
"type": "string"
},
"username": {
"description": "TODO: Remove the omitempty tags at a later point",
"type": "string"
}
}
+133 -1
View File
@@ -1,5 +1,24 @@
basePath: /api/v1
definitions:
git_kundeng_us_phoenix_textsender-models_tx0_user.User:
properties:
created:
type: string
first_name:
type: string
id:
type: string
last_login:
type: string
last_name:
type: string
password:
type: string
phone_number:
type: string
username:
type: string
type: object
handler.LoginAccount:
properties:
password:
@@ -89,24 +108,69 @@ definitions:
message:
type: string
type: object
handler.UpdateNameRequest:
properties:
first_name:
type: string
last_name:
type: string
user_id:
type: string
type: object
handler.UpdateNameResponse:
properties:
data:
items:
$ref: '#/definitions/git_kundeng_us_phoenix_textsender-models_tx0_user.User'
type: array
message:
type: string
type: object
handler.UpdatePasswordRequest:
properties:
confirmed_password:
type: string
current_password:
type: string
updated_password:
type: string
user_id:
type: string
type: object
handler.UpdatePasswordResponse:
properties:
data:
items:
$ref: '#/definitions/git_kundeng_us_phoenix_textsender-models_tx0_user.User'
type: array
message:
type: string
type: object
token.Login:
properties:
access_token:
type: string
expires_in:
type: integer
issued_at:
type: integer
token_type:
type: string
user_id:
type: string
type: object
user.ServiceUser:
properties:
date_created:
created:
type: string
id:
type: string
last_login:
type: string
passphrase:
type: string
username:
description: 'TODO: Remove the omitempty tags at a later point'
type: string
type: object
host: localhost:9080
@@ -280,6 +344,74 @@ paths:
summary: Obtain a refresh token
tags:
- refresh
/user/name/update:
patch:
consumes:
- application/json
description: Update the first or last name of a user (requires JWT)
parameters:
- description: Data to update name of user
in: body
name: request
required: true
schema:
$ref: '#/definitions/handler.UpdateNameRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.UpdateNameResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.UpdateNameResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/handler.UpdateNameResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.UpdateNameResponse'
security:
- BearerAuth: []
summary: Update name of user
tags:
- users
/user/password/update:
patch:
consumes:
- application/json
description: Update the password of a regular account (requires JWT)
parameters:
- description: Needed data to update password
in: body
name: request
required: true
schema:
$ref: '#/definitions/handler.UpdatePasswordRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.UpdatePasswordResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.UpdatePasswordResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.UpdatePasswordResponse'
security:
- BearerAuth: []
summary: Update Password
tags:
- users
securityDefinitions:
BearerAuth:
description: JWT Bearer Token
+23 -19
View File
@@ -1,41 +1,45 @@
module git.kundeng.us/phoenix/textsender-auth
go 1.25.4
go 1.26.2
require (
git.kundeng.us/phoenix/textsender-models v0.0.12
github.com/go-chi/chi/v5 v5.2.3
git.kundeng.us/phoenix/textsender-models v0.2.1
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/cors v1.2.2
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/jackc/pgx/v5 v5.7.5
github.com/jackc/pgx/v5 v5.9.2
github.com/joho/godotenv v1.5.1
github.com/stretchr/testify v1.11.1
github.com/swaggo/http-swagger/v2 v2.0.2
github.com/swaggo/swag v1.16.6
golang.org/x/crypto v0.42.0
golang.org/x/crypto v0.50.0
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.6 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-openapi/jsonpointer v0.23.1 // indirect
github.com/go-openapi/jsonreference v0.21.5 // indirect
github.com/go-openapi/spec v0.22.4 // indirect
github.com/go-openapi/swag/conv v0.26.0 // indirect
github.com/go-openapi/swag/jsonname v0.26.0 // indirect
github.com/go-openapi/swag/jsonutils v0.26.0 // indirect
github.com/go-openapi/swag/loading v0.26.0 // indirect
github.com/go-openapi/swag/stringutils v0.26.0 // indirect
github.com/go-openapi/swag/typeutils v0.26.0 // indirect
github.com/go-openapi/swag/yamlutils v0.26.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
github.com/swaggo/files/v2 v2.0.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.44.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+48 -48
View File
@@ -1,92 +1,92 @@
git.kundeng.us/phoenix/textsender-models v0.0.12 h1:ps9H3FS5LyCwQhAiIvg4vYyfLZZ64dex1y9ytb0o9C4=
git.kundeng.us/phoenix/textsender-models v0.0.12/go.mod h1:9iPDQJg1Tc6WMNoW5+f8YKmnosMwlWHJ++hmxNLDEe0=
git.kundeng.us/phoenix/textsender-models v0.2.1 h1:21br4NF58aUFuCx8laKxC5RvZMl4GsSIaMX4bvf5plw=
git.kundeng.us/phoenix/textsender-models v0.2.1/go.mod h1:nu5QWy9o+spx/t9NFipaGmF5qiBJS/0QhxyCjoi3Z3E=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ=
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I=
github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE=
github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w=
github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M=
github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA=
github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y=
github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko=
github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg=
github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg=
github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE=
github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4=
github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE=
github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ=
github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE=
github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4=
github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
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-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg=
github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+12 -6
View File
@@ -1,8 +1,14 @@
package endpoint
// Endpoint for registering a user
const Register = "/api/v1/register"
const Login = "/api/v1/login"
const CreateServiceUser = "/api/v1/service/register"
const LoginServiceUser = "/api/v1/service/login"
const TokenRefresh = "/api/v1/token/refresh"
const (
// Endpoint for registering a user
Register = "/api/v1/register"
Login = "/api/v1/login"
UpdatePassword = "/api/v1/user/password/update"
UpdateName = "/api/v1/user/name/update"
CreateServiceUser = "/api/v1/service/register"
LoginServiceUser = "/api/v1/service/login"
TokenRefresh = "/api/v1/token/refresh"
)
+125 -27
View File
@@ -1,16 +1,28 @@
package handler
import (
"fmt"
"log"
"net/http"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/token"
"git.kundeng.us/phoenix/textsender-models/tx0/user"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-auth/internal/config"
"git.kundeng.us/phoenix/textsender-auth/internal/store"
"git.kundeng.us/phoenix/textsender-auth/internal/utility"
)
type LoginHandler struct {
Config *config.Config
UserStore store.UserStore
}
func NewLoginHandler(cfg *config.Config, userStore store.UserStore) *LoginHandler {
return &LoginHandler{Config: cfg, UserStore: userStore}
}
type LoginAccount struct {
Username string `json:"username"`
Password string `json:"password"`
@@ -21,15 +33,6 @@ type LoginResponse struct {
Data []token.Login `json:"data"`
}
type LoginHandler struct {
Config *config.Config
UserStore store.UserStore
}
func NewLoginHandler(cfg *config.Config, userStore store.UserStore) *LoginHandler {
return &LoginHandler{Config: cfg, UserStore: userStore}
}
// Login godoc
// @Summary Login
// @Description Login and be given an access token (requires JWT)
@@ -55,7 +58,7 @@ func (l *LoginHandler) Login(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if exists, err := l.UserStore.UserExists(ctx, req.Username); err != nil {
fmt.Printf("Error: %v", err)
log.Println("Error:", err)
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
@@ -67,23 +70,36 @@ func (l *LoginHandler) Login(w http.ResponseWriter, r *http.Request) {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
hashing := utility.HashMash{Password: req.Password}
if hashing.CheckPasswordHash(req.Password, user.Password) {
var tokGen utility.TokenGenerator
secretKey := config.GetSecretKey()
tokGen.SetSecretKey(secretKey)
if myToken, err := tokGen.GenerateToken(*user); err != nil {
fmt.Println(err.Error())
statusCode = http.StatusInternalServerError
resp.Message = "Error generating token"
} else {
statusCode = http.StatusOK
resp.Data = append(resp.Data, *myToken)
resp.Message = "Successful"
}
hashing := utility.HashMash{}
if err := hashing.SetPassword(req.Password); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
statusCode = http.StatusNotFound
resp.Message = "User not found"
if hashing.CheckPasswordHash(req.Password, user.Password) {
lastLogin := time.Now()
var tokGen utility.TokenGenerator
secretKey := config.GetSecretKey()
tokGen.SetSecretKey(secretKey)
if myToken, err := tokGen.GenerateToken(*user); err != nil {
log.Println(err.Error())
statusCode = http.StatusInternalServerError
resp.Message = "Error generating token"
} else {
log.Println("Updating user's last login")
if rowsAffected, err := l.UserStore.UpdateLastLogin(ctx, user.Id, lastLogin); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
log.Println("Rows updated:", rowsAffected)
statusCode = http.StatusOK
resp.Data = append(resp.Data, *myToken)
resp.Message = "Successful"
}
}
} else {
statusCode = http.StatusNotFound
resp.Message = "User not found"
}
}
}
}
@@ -91,3 +107,85 @@ func (l *LoginHandler) Login(w http.ResponseWriter, r *http.Request) {
RespondWithJson(w, statusCode, &resp)
}
type UpdatePasswordRequest struct {
UserId uuid.UUID `json:"user_id"`
CurrentPassword string `json:"current_password"`
UpdatedPassword string `json:"updated_password"`
ConfirmedPassword string `json:"confirmed_password"`
}
type UpdatePasswordResponse struct {
Message string `json:"message"`
Data []user.User `json:"data"`
}
// UpdatePassword godoc
// @Summary Update Password
// @Description Update the password of a regular account (requires JWT)
// @Tags users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body UpdatePasswordRequest true "Needed data to update password"
// @Success 200 {object} UpdatePasswordResponse
// @Failure 400 {object} UpdatePasswordResponse
// @Failure 500 {object} UpdatePasswordResponse
// @Router /user/password/update [patch]
func (l *LoginHandler) UpdatePassword(w http.ResponseWriter, r *http.Request) {
var req UpdatePasswordRequest
if err := ExtractFromRequest(r, &req); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
}
defer r.Body.Close()
var statusCode int
var resp UpdatePasswordResponse
ctx := r.Context()
if usr, err := l.UserStore.GetUserByID(ctx, req.UserId); err != nil {
log.Println("Error:", err)
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
hashing := utility.HashMash{}
if err := hashing.SetPassword(req.CurrentPassword); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if hashing.CheckPasswordHash(req.CurrentPassword, usr.Password) {
if req.UpdatedPassword == req.ConfirmedPassword {
// Hash password
err := hashing.SetPassword(req.UpdatedPassword)
hashedPassword, err := hashing.HashPassword()
if err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
// Update user password
usr.Password = hashedPassword
// Save user in DB
if rowsAffected, err := l.UserStore.UpdatePassword(ctx, usr.Id, usr.Password); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
log.Println("Rows affected:", rowsAffected)
statusCode = http.StatusOK
resp.Message = "Successful"
resp.Data = append(resp.Data, *usr)
}
}
} else {
statusCode = http.StatusBadRequest
resp.Message = "Passwords do not match"
}
} else {
statusCode = http.StatusBadRequest
resp.Message = "User not found"
}
}
}
RespondWithJson(w, statusCode, &resp)
}
+29 -11
View File
@@ -13,7 +13,6 @@ import (
"git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint"
"git.kundeng.us/phoenix/textsender-auth/internal/store/mock"
"git.kundeng.us/phoenix/textsender-auth/internal/utility"
)
func TestLogin(t *testing.T) {
@@ -21,20 +20,13 @@ func TestLogin(t *testing.T) {
mockstore := mock.NewMockUserStore()
handler := NewLoginHandler(cfg, mockstore)
testUser := GetTestUser()
unhashedPassword := testUser.Password
hashing := utility.HashMash{Password: testUser.Password}
hashedPassword, err := hashing.HashPassword()
assert.NoError(t, err)
testUser.Password = hashedPassword
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
mockstore.CreateUser(ctx, &testUser)
testUser, unhashedPassword, err := createUser(ctx, mockstore)
assert.NoError(t, err, "Error Creating user")
loginUser := LoginAccount{Username: testUser.Username, Password: unhashedPassword}
loginUser := LoginAccount{Username: testUser.Username, Password: *unhashedPassword}
jsonValue, _ := json.Marshal(loginUser)
req, _ := http.NewRequest("POST", endpoint.Login, strings.NewReader(string(jsonValue)))
@@ -50,3 +42,29 @@ func TestLogin(t *testing.T) {
assert.NotEmpty(t, response.Data, "An access token should have been returned")
}
func TestUpdatePassword(t *testing.T) {
cfg := GetConfig()
mockstore := mock.NewMockUserStore()
handler := NewLoginHandler(cfg, mockstore)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
testUser, unhashedPassword, err := createUser(ctx, mockstore)
assert.NoError(t, err, "Error Creating user")
assert.NotNil(t, testUser, "User should not be nil")
updatedPassword := "TakeATrip2yonder!"
newPassword := UpdatePasswordRequest{UserId: testUser.Id, CurrentPassword: *unhashedPassword, UpdatedPassword: updatedPassword, ConfirmedPassword: updatedPassword}
jsonValue, _ := json.Marshal(newPassword)
req, _ := http.NewRequest("PATCH", endpoint.UpdatePassword, strings.NewReader(string(jsonValue)))
rr := httptest.NewRecorder()
handler.UpdatePassword(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response UpdatePasswordResponse
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err)
}
+6 -2
View File
@@ -19,8 +19,12 @@ func TestRefreshTokenWithMock(t *testing.T) {
var serviceUser user.ServiceUser
var hashedPassword string
var err error
unhashed := "9328nr29nudx3292m320!"
hashing := utility.HashMash{Password: unhashed}
unhashed := "A9328nr29nudx3292m320!"
hashing := utility.HashMash{}
if err := hashing.SetPassword(unhashed); err != nil {
assert.NoError(t, err, "Error setting password")
}
if hashedPassword, err = hashing.HashPassword(); err != nil {
assert.NoError(t, err, "Error hashing password: %v", err)
} else {
+96 -17
View File
@@ -2,6 +2,7 @@ package handler
import (
"fmt"
"log"
"net/http"
"git.kundeng.us/phoenix/textsender-models/tx0/user"
@@ -12,6 +13,15 @@ import (
"git.kundeng.us/phoenix/textsender-auth/internal/utility"
)
type UserHandler struct {
Config *config.Config
UserStore store.UserStore
}
func NewUserHandler(cfg *config.Config, userStore store.UserStore) *UserHandler {
return &UserHandler{Config: cfg, UserStore: userStore}
}
type RegisterUser struct {
PhoneNumber string `json:"phone_number"`
Username string `json:"username"`
@@ -29,15 +39,6 @@ type RegisterResponse struct {
Data []RegisterResponseItem `json:"data"`
}
type UserHandler struct {
Config *config.Config
UserStore store.UserStore
}
func NewUserHandler(cfg *config.Config, userStore store.UserStore) *UserHandler {
return &UserHandler{Config: cfg, UserStore: userStore}
}
// Register godoc
// @Summary Register user
// @Description Create a user that can send texts (requires JWT)
@@ -85,20 +86,25 @@ func (u *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
statusCode = http.StatusBadRequest
resp.Message = "Failure in creating User"
} else {
hashing := utility.HashMash{Password: user.Password}
if hashedPassword, err := hashing.HashPassword(); err != nil {
hashing := utility.HashMash{}
if err := hashing.SetPassword(req.Password); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
user.Password = hashedPassword
err := u.UserStore.CreateUser(ctx, &user)
if err != nil {
if hashedPassword, err := hashing.HashPassword(); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
resp.Message = "Successful"
statusCode = http.StatusOK
resp.Data = append(resp.Data, RegisterResponseItem{Id: user.Id, PhoneNumber: user.PhoneNumber, Username: user.Username})
user.Password = hashedPassword
err := u.UserStore.CreateUser(ctx, &user)
if err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
resp.Message = "Successful"
statusCode = http.StatusOK
resp.Data = append(resp.Data, RegisterResponseItem{Id: user.Id, PhoneNumber: user.PhoneNumber, Username: user.Username})
}
}
}
}
@@ -107,3 +113,76 @@ func (u *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
RespondWithJson(w, statusCode, &resp)
}
type UpdateNameRequest struct {
Firstname *string `json:"first_name,omitempty"`
Lastname *string `json:"last_name,omitempty"`
UserId uuid.UUID `json:"user_id"`
}
type UpdateNameResponse struct {
Message string `json:"message"`
Data []*user.User `json:"data"`
}
// UpdateName godoc
// @Summary Update name of user
// @Description Update the first or last name of a user (requires JWT)
// @Tags users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body UpdateNameRequest true "Data to update name of user"
// @Success 200 {object} UpdateNameResponse
// @Failure 400 {object} UpdateNameResponse
// @Failure 403 {object} UpdateNameResponse
// @Failure 500 {object} UpdateNameResponse
// @Router /user/name/update [patch]
func (u *UserHandler) UpdateName(w http.ResponseWriter, r *http.Request) {
var req UpdateNameRequest
err := ExtractFromRequest(r, &req)
if err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
return
}
defer r.Body.Close()
var statusCode int
var resp UpdateNameResponse
updateFirstname := req.Firstname != nil && len(*req.Firstname) > 0
updateLastname := req.Lastname != nil && len(*req.Lastname) > 0
if req.UserId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "User Id not provided"
} else if !updateFirstname && !updateLastname {
statusCode = http.StatusBadRequest
resp.Message = "No name provided"
} else {
ctx := r.Context()
if usr, err := u.UserStore.GetUserByID(ctx, req.UserId); err != nil {
log.Println("Error:", err)
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if usr == nil {
statusCode = http.StatusNotFound
resp.Message = "User not found"
} else {
// Add query to update names
if rowsAffected, err := u.UserStore.UpdateName(ctx, req.Firstname, req.Lastname, usr); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
log.Println("Rows updated:", rowsAffected)
statusCode = http.StatusOK
resp.Message = "Successful"
resp.Data = append(resp.Data, usr)
}
}
}
}
RespondWithJson(w, statusCode, &resp)
}
+29
View File
@@ -34,3 +34,32 @@ func TestCreateUserWithMock(t *testing.T) {
assert.NotNil(t, response.Data[0].Id, "Id should not be nil")
}
func TestUpdateNameOfUser(t *testing.T) {
ctx := t.Context()
cfg := GetConfig()
userStore := mock.NewMockUserStore()
usr := GetTestUser()
if err := userStore.CreateUser(ctx, &usr); err != nil {
assert.NoError(t, err, "Error creating user")
}
testNameChange := UpdateNameRequest{}
testNameChange.Firstname = &[]string{"Bob"}[0]
testNameChange.Lastname = &[]string{"De-Buildor"}[0]
testNameChange.UserId = usr.Id
jsonValue, _ := json.Marshal(testNameChange)
req, _ := http.NewRequest("PATCH", endpoint.UpdateName, strings.NewReader(string(jsonValue)))
rr := httptest.NewRecorder()
handler := NewUserHandler(cfg, userStore)
handler.UpdateName(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response UpdateNameResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err)
}
+40 -20
View File
@@ -1,7 +1,9 @@
package handler
import (
"log"
"net/http"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/token"
"git.kundeng.us/phoenix/textsender-models/tx0/user"
@@ -70,19 +72,24 @@ func (s *ServiceHandler) Register(w http.ResponseWriter, r *http.Request) {
statusCode = http.StatusBadRequest
resp.Message = "Service user already exists"
} else {
hashing := utility.HashMash{Password: req.Passphrase}
if hashedPassword, err := hashing.HashPassword(); err != nil {
hashing := utility.HashMash{}
if err := hashing.SetPassword(req.Passphrase); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
serviceUser := user.ServiceUser{Username: req.Username, Passphrase: hashedPassword}
if err := s.ServiceStore.Create(ctx, &serviceUser); err != nil {
if hashedPassword, err := hashing.HashPassword(); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
statusCode = http.StatusCreated
resp.Message = "Successful"
resp.Data = append(resp.Data, &serviceUser)
serviceUser := user.ServiceUser{Username: req.Username, Passphrase: hashedPassword}
if err := s.ServiceStore.Create(ctx, &serviceUser); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
statusCode = http.StatusCreated
resp.Message = "Successful"
resp.Data = append(resp.Data, &serviceUser)
}
}
}
}
@@ -139,23 +146,36 @@ func (s *ServiceHandler) Login(w http.ResponseWriter, r *http.Request) {
statusCode = http.StatusNotFound
resp.Message = "Not found"
} else {
hashing := utility.HashMash{Password: req.Passphrase}
if !hashing.CheckPasswordHash(req.Passphrase, serviceUser.Passphrase) {
hashing := utility.HashMash{}
if err := hashing.SetPassword(req.Passphrase); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = "Not valid"
resp.Message = err.Error()
} else {
var tokGen utility.TokenGenerator
tokGen.SetHourOffset(8)
secretKey := config.GetSecretKey()
tokGen.SetSecretKey(secretKey)
if myToken, err := tokGen.GenerateToken(*serviceUser); err != nil {
if !hashing.CheckPasswordHash(req.Passphrase, serviceUser.Passphrase) {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
resp.Message = "Not valid"
} else {
statusCode = http.StatusOK
resp.Data = append(resp.Data, myToken)
resp.Message = "Successful"
lastLogin := time.Now()
var tokGen utility.TokenGenerator
tokGen.SetHourOffset(8)
secretKey := config.GetSecretKey()
tokGen.SetSecretKey(secretKey)
if myToken, err := tokGen.GenerateToken(*serviceUser); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
log.Println("Updating service user's last login")
if rowsAffected, err := s.ServiceStore.UpdateLastLogin(ctx, serviceUser.Id, lastLogin); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
log.Println("Rows updated:", rowsAffected)
statusCode = http.StatusOK
resp.Data = append(resp.Data, myToken)
resp.Message = "Successful"
}
}
}
}
}
+7 -3
View File
@@ -20,7 +20,7 @@ func TestCreateServiceUserWithMock(t *testing.T) {
mockStore := mock.NewMockServiceUserStore()
handler := NewServiceHandler(cfg, mockStore)
testService := ServiceCreationRequest{Username: "swoon", Passphrase: "ewrewr329n12y3x2!2"}
testService := ServiceCreationRequest{Username: "swoon", Passphrase: "Ewrewr329n12y3x2!2"}
jsonValue, err := json.Marshal(testService)
assert.NoError(t, err, "Error marshaling request")
@@ -39,8 +39,12 @@ func TestLoginServiceUserWithMock(t *testing.T) {
var serviceUser user.ServiceUser
var hashedPassword string
var err error
unhashed := "9328nr29nudx3292m320!"
hashing := utility.HashMash{Password: unhashed}
unhashed := "A9328nr29nudx3292m320!"
hashing := utility.HashMash{}
if err := hashing.SetPassword(unhashed); err != nil {
assert.NoError(t, err, "Error setting password")
}
if hashedPassword, err = hashing.HashPassword(); err != nil {
assert.NoError(t, err, "Error hashing password: %v", err)
} else {
+24 -1
View File
@@ -1,6 +1,8 @@
package handler
import (
"context"
"fmt"
"log"
"os"
"path"
@@ -9,10 +11,12 @@ import (
"github.com/joho/godotenv"
"git.kundeng.us/phoenix/textsender-auth/internal/config"
"git.kundeng.us/phoenix/textsender-auth/internal/store/mock"
"git.kundeng.us/phoenix/textsender-auth/internal/utility"
)
func GetTestUser() user.User {
return user.User{Username: "ghost", PhoneNumber: "+1234567890", Password: "dfgdffddfd"}
return user.User{Username: "ghost", PhoneNumber: "+1234567890", Password: "Dfgdffd343dfd!"}
}
func GetConfig() *config.Config {
@@ -38,3 +42,22 @@ func GetConfig() *config.Config {
EnableRegistration: config.CheckRegistration(),
}
}
func createUser(ctx context.Context, userStore *mock.MockUserStore) (*user.User, *string, error) {
testUser := GetTestUser()
unhashedPassword := testUser.Password
hashing := utility.HashMash{}
if err := hashing.SetPassword(testUser.Password); err != nil {
return nil, nil, fmt.Errorf("Error setting password: %v", err)
}
hashedPassword, err := hashing.HashPassword()
if err != nil {
return nil, nil, err
}
testUser.Password = hashedPassword
userStore.CreateUser(ctx, &testUser)
return &testUser, &unhashedPassword, nil
}
+47
View File
@@ -0,0 +1,47 @@
package middleware
import (
"context"
"net/http"
"strings"
"git.kundeng.us/phoenix/textsender-auth/internal/services"
)
type contextKey string
const (
UserContextKey contextKey = "user"
)
func AuthMiddleware(authService *services.JWTService) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
// Extract token from "Bearer <token>"
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
return
}
token := parts[1]
// Validate token with auth service
user, err := authService.ValidateToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Add user to context
ctx := context.WithValue(r.Context(), UserContextKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
+49
View File
@@ -0,0 +1,49 @@
package services
import (
"time"
txtmodels_token "git.kundeng.us/phoenix/textsender-models/tx0/token"
txtmodels_user "git.kundeng.us/phoenix/textsender-models/tx0/user"
"github.com/golang-jwt/jwt/v5"
)
type JWTService struct {
secretKey []byte
}
func NewJWTService(secretKey string) *JWTService {
return &JWTService{
secretKey: []byte(secretKey),
}
}
func (s *JWTService) ValidateToken(tokenString string) (*txtmodels_user.User, error) {
// TODO: Include more user information in the claims to populate user
claims := &txtmodels_token.Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
// Validate the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return s.secretKey, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, jwt.ErrSignatureInvalid
}
// Check token expiration
if time.Now().After(claims.ExpiresAt.Time) {
return nil, jwt.ErrTokenExpired
}
return &txtmodels_user.User{
Id: claims.UserId,
}, nil
}
+22
View File
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"sync"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/user"
"github.com/google/uuid"
@@ -101,3 +102,24 @@ func (m *MockServiceUserStore) GetWithId(ctx context.Context, id uuid.UUID) (*us
return nil, nil
}
func (m *MockServiceUserStore) UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLogin time.Time) (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return 0, m.Error
}
serviceUser, exists := m.ServiceUsers[id]
if !exists {
return 0, errors.New("Service user not found")
}
serviceUser.LastLogin = &lastLogin
m.ServiceUsers[id] = serviceUser
m.ServiceUsersByUsername[serviceUser.Username] = serviceUser
return 1, nil
}
@@ -3,7 +3,9 @@ package mock
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/google/uuid"
@@ -37,9 +39,11 @@ func (m *MockUserStore) CreateUser(ctx context.Context, user *user.User) error {
}
if _, exists := m.UsersByUsername[user.Username]; exists {
return errors.New("User with email already exists")
return errors.New("User with username already exists")
}
user.Created = time.Now()
m.Users[user.Id] = user
m.UsersByUsername[user.Username] = user
return nil
@@ -112,3 +116,77 @@ func (m *MockUserStore) UserExists(ctx context.Context, username string) (bool,
return exists, nil
}
}
func (m *MockUserStore) UpdatePassword(ctx context.Context, id uuid.UUID, password string) (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return 0, m.Error
}
user, exists := m.Users[id]
if !exists {
return 0, errors.New("User not found")
}
user.Password = password
m.Users[id] = user
m.UsersByUsername[user.Username] = user
return 1, nil
}
func (m *MockUserStore) UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLogin time.Time) (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return 0, m.Error
}
user, exists := m.Users[id]
if !exists {
return 0, errors.New("User not found")
}
user.LastLogin = &lastLogin
m.Users[id] = user
m.UsersByUsername[user.Username] = user
return 1, nil
}
func (m *MockUserStore) UpdateName(ctx context.Context, firstname *string, lastname *string, usr *user.User) (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return 0, m.Error
}
if firstname == nil && lastname == nil {
return 0, fmt.Errorf("Names not provided")
}
user, exists := m.Users[usr.Id]
if !exists {
return 0, errors.New("User not found")
}
if firstname != nil && lastname != nil {
user.Firstname = firstname
user.Lastname = lastname
} else if firstname != nil {
user.Firstname = firstname
} else {
user.Lastname = lastname
}
m.Users[usr.Id] = user
m.UsersByUsername[user.Username] = user
return 1, nil
}
+12
View File
@@ -3,6 +3,7 @@ package store
import (
"context"
"fmt"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/user"
"github.com/google/uuid"
@@ -10,11 +11,13 @@ import (
"github.com/jackc/pgx/v5/pgxpool"
)
// TODO: Rename this to ServiceUserStore
type ServiceStore interface {
CheckWithUsername(ctx context.Context, username string) (bool, error)
GetWithUsername(ctx context.Context, username string) (*user.ServiceUser, error)
GetWithId(ctx context.Context, id uuid.UUID) (*user.ServiceUser, error)
Create(ctx context.Context, serviceUser *user.ServiceUser) error
UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLogin time.Time) (int64, error)
}
type PGServiceStore struct {
@@ -81,3 +84,12 @@ func (s *PGServiceStore) Create(ctx context.Context, serviceUser *user.ServiceUs
&serviceUser.Id, &serviceUser.Created,
)
}
func (s *PGServiceStore) UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLogin time.Time) (int64, error) {
query := `UPDATE service_users SET last_login = $1 WHERE id = $2`
if affected, err := s.db.Exec(ctx, query, lastLogin, id); err != nil {
return 0, err
} else {
return affected.RowsAffected(), nil
}
}
+66 -8
View File
@@ -3,6 +3,7 @@ package store
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
@@ -17,6 +18,9 @@ type UserStore interface {
GetUserByUsername(ctx context.Context, username string) (*user.User, error)
GetAllUsers(ctx context.Context) ([]*user.User, error)
UserExists(ctx context.Context, username string) (bool, error)
UpdatePassword(ctx context.Context, id uuid.UUID, password string) (int64, error)
UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLogin time.Time) (int64, error)
UpdateName(ctx context.Context, firstname *string, lastname *string, usr *user.User) (int64, error)
}
type PGUserStore struct {
@@ -31,20 +35,20 @@ func (s *PGUserStore) CreateUser(ctx context.Context, user *user.User) error {
query := `
INSERT INTO users (phone_number, username, password)
VALUES ($1, $2, $3)
RETURNING id, phone_number, username
RETURNING id, phone_number, username, created
`
return s.db.QueryRow(ctx, query, user.PhoneNumber, user.Username, user.Password).Scan(
&user.Id, &user.PhoneNumber, &user.Username,
&user.Id, &user.PhoneNumber, &user.Username, &user.Created,
)
}
func (s *PGUserStore) GetUserByID(ctx context.Context, id uuid.UUID) (*user.User, error) {
query := `SELECT id, username, password, phone_number FROM users WHERE id = $1`
query := `SELECT id, username, password, phone_number, created FROM users WHERE id = $1`
var user user.User
err := s.db.QueryRow(ctx, query, id).Scan(
&user.Id, &user.Username, &user.Password, &user.PhoneNumber,
&user.Id, &user.Username, &user.Password, &user.PhoneNumber, &user.Created,
)
if err == pgx.ErrNoRows {
@@ -58,11 +62,11 @@ func (s *PGUserStore) GetUserByID(ctx context.Context, id uuid.UUID) (*user.User
}
func (s *PGUserStore) GetUserByUsername(ctx context.Context, username string) (*user.User, error) {
query := `SELECT id, username, password, phone_number FROM users WHERE username = $1`
query := `SELECT id, username, password, phone_number, created FROM users WHERE username = $1`
var user user.User
err := s.db.QueryRow(ctx, query, username).Scan(
&user.Id, &user.Username, &user.Password, &user.PhoneNumber,
&user.Id, &user.Username, &user.Password, &user.PhoneNumber, &user.Created,
)
if err == pgx.ErrNoRows {
@@ -76,7 +80,7 @@ func (s *PGUserStore) GetUserByUsername(ctx context.Context, username string) (*
}
func (s *PGUserStore) GetAllUsers(ctx context.Context) ([]*user.User, error) {
query := `SELECT id, username, password, phone_number FROM users`
query := `SELECT id, username, password, phone_number, created FROM users`
rows, err := s.db.Query(ctx, query)
if err != nil {
@@ -88,7 +92,7 @@ func (s *PGUserStore) GetAllUsers(ctx context.Context) ([]*user.User, error) {
for rows.Next() {
var user user.User
if err := rows.Scan(
&user.Id, &user.Username, &user.Password, &user.PhoneNumber,
&user.Id, &user.Username, &user.Password, &user.PhoneNumber, &user.Created,
); err != nil {
return nil, fmt.Errorf("scanning user row: %w", err)
}
@@ -113,3 +117,57 @@ func (s *PGUserStore) UserExists(ctx context.Context, username string) (bool, er
return exists, nil
}
func (s *PGUserStore) UpdatePassword(ctx context.Context, id uuid.UUID, password string) (int64, error) {
query := `UPDATE users SET password = $1 WHERE id = $2`
if affected, err := s.db.Exec(ctx, query, password, id); err != nil {
return 0, err
} else {
return affected.RowsAffected(), nil
}
}
func (s *PGUserStore) UpdateLastLogin(ctx context.Context, id uuid.UUID, lastLogin time.Time) (int64, error) {
query := `UPDATE users SET last_login = $1 WHERE id = $2`
if affected, err := s.db.Exec(ctx, query, lastLogin, id); err != nil {
return 0, err
} else {
return affected.RowsAffected(), nil
}
}
func (s *PGUserStore) UpdateName(ctx context.Context, firstname *string, lastname *string, usr *user.User) (int64, error) {
var query string
if firstname == nil && lastname == nil {
return 0, fmt.Errorf("Provided names are empty")
} else {
tableName := "users"
if firstname != nil && lastname != nil {
query = fmt.Sprintf("UPDATE %s SET first_name = $1, last_name = $2 WHERE id = $3", tableName)
if affected, err := s.db.Exec(ctx, query, firstname, lastname, usr.Id); err != nil {
return 0, err
} else {
usr.Firstname = firstname
usr.Lastname = lastname
return affected.RowsAffected(), nil
}
} else if firstname != nil {
query = fmt.Sprintf("UPDATE %s SET first_name = $1 WHERE id = $2", tableName)
if affected, err := s.db.Exec(ctx, query, firstname, usr.Id); err != nil {
return 0, err
} else {
usr.Firstname = firstname
return affected.RowsAffected(), nil
}
} else {
query = fmt.Sprintf("UPDATE %s SET last_name = $1 WHERE id = $2", tableName)
if affected, err := s.db.Exec(ctx, query, lastname, usr.Id); err != nil {
return 0, err
} else {
usr.Lastname = lastname
return affected.RowsAffected(), nil
}
}
}
}
+43 -4
View File
@@ -1,15 +1,19 @@
package utility
import (
"fmt"
"strings"
"unicode"
"golang.org/x/crypto/bcrypt"
)
type HashMash struct {
Password string
password string
}
func (h *HashMash) HashPassword() (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(h.Password), bcrypt.DefaultCost)
bytes, err := bcrypt.GenerateFromPassword([]byte(h.password), bcrypt.DefaultCost)
return string(bytes), err
}
@@ -18,6 +22,41 @@ func (h *HashMash) CheckPasswordHash(password string, hash string) bool {
return err == nil
}
func (h *HashMash) SetPassword(password string) {
h.Password = password
func (h *HashMash) SetPassword(password string) error {
if len(password) < 8 {
return fmt.Errorf("Password length is not enought")
} else if len(password) > 32 {
return fmt.Errorf("Password length is too long")
} else {
specialCharacters := "!@#$%^&*?"
numbers := "0123456789"
if strings.ContainsAny(password, specialCharacters) && strings.ContainsAny(password, numbers) {
var hasAtleastOneUpper, hasAtleastOneLower bool
for _, c := range password {
if unicode.IsUpper(c) {
hasAtleastOneUpper = true
} else if unicode.IsLower(c) {
hasAtleastOneLower = true
}
if hasAtleastOneLower && hasAtleastOneUpper {
break
}
}
if hasAtleastOneUpper && hasAtleastOneLower {
h.password = password
return nil
} else {
if !hasAtleastOneUpper {
return fmt.Errorf("Password requires at least one upper case letter")
} else {
return fmt.Errorf("Password requires at least one lower case letter")
}
}
} else {
return fmt.Errorf("Password should contain special characters and numbers")
}
}
}
+7 -16
View File
@@ -38,7 +38,6 @@ func (t *TokenGenerator) GenerateToken(usr any) (*token.Login, error) {
if t.hourOffset == 0 {
t.hourOffset = 4
}
expirationTime := time.Now().Add(t.hourOffset * time.Hour)
if claims, err := t.generateClaims(usr, TOKEN_TYPE, issuedAt, expirationTime); err != nil {
@@ -48,7 +47,7 @@ func (t *TokenGenerator) GenerateToken(usr any) (*token.Login, error) {
if tokenString, err := myToken.SignedString(t.SecretKey); err != nil {
return nil, err
} else {
return &token.Login{UserId: claims.UserId, AccessToken: tokenString, TokenType: TOKEN_TYPE, ExpiresIn: expirationTime.Unix()}, nil
return &token.Login{UserId: claims.UserId, AccessToken: tokenString, TokenType: TOKEN_TYPE, ExpiresIn: expirationTime.Unix(), IssuedAt: issuedAt.Unix()}, nil
}
}
}
@@ -60,14 +59,10 @@ func (t *TokenGenerator) VerifyToken(accessToken string) (bool, error) {
return false, nil
} else {
if tken.Valid {
if clms != nil {
if clms.UserId != uuid.Nil {
return true, nil
} else {
return false, fmt.Errorf("User Id was not set")
}
if clms.UserId != uuid.Nil {
return true, nil
} else {
return false, fmt.Errorf("Claims not parsed")
return false, fmt.Errorf("User Id was not set")
}
} else {
return false, fmt.Errorf("Invalid access token")
@@ -82,14 +77,10 @@ func (t *TokenGenerator) ExtractIdFromToken(accessToken string) (uuid.UUID, erro
return uuid.Nil, nil
} else {
if tken.Valid {
if clms != nil {
if clms.UserId != uuid.Nil {
return clms.UserId, nil
} else {
return uuid.Nil, fmt.Errorf("User Id was not set")
}
if clms.UserId != uuid.Nil {
return clms.UserId, nil
} else {
return uuid.Nil, fmt.Errorf("Claims not parsed")
return uuid.Nil, fmt.Errorf("User Id was not set")
}
} else {
return uuid.Nil, fmt.Errorf("Invalid access token")
+7 -2
View File
@@ -5,14 +5,19 @@ DROP TABLE IF EXISTS service_users CASCADE;
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
first_name TEXT NULL,
last_name TEXT NULL,
phone_number TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL
password TEXT NOT NULL,
created timestamptz DEFAULT now(),
last_login timestamptz NULL
);
CREATE TABLE service_users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
username TEXT NOT NULL,
passphrase TEXT NOT NULL,
created timestamptz DEFAULT now()
created timestamptz DEFAULT now(),
last_login timestamptz NULL
);