initial commit, adding SQL connection and a simple testing on main

This commit is contained in:
Josepablo C.
2025-12-07 08:51:10 -06:00
commit bf78edeec0
16 changed files with 2406 additions and 0 deletions

29
app/go.mod Normal file
View File

@@ -0,0 +1,29 @@
module cloud.etaviaporte.com/api
go 1.23.2
require (
github.com/joho/godotenv v1.5.1
github.com/spf13/viper v1.21.0
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.28.0 // indirect
)

61
app/go.sum Normal file
View File

@@ -0,0 +1,61 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
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/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

42
app/libs/database/db.go Normal file
View File

@@ -0,0 +1,42 @@
package database
import (
"log"
"github.com/joho/godotenv"
"github.com/spf13/viper"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var shared_db_connection *gorm.DB
func initDB() *gorm.DB {
// load .env into environment (so viper can read them)
if err := godotenv.Load(); err != nil {
log.Println(".env not found or could not be loaded; proceeding with existing environment variables")
}
viper.AutomaticEnv() // read from environment
sql_dsn := viper.GetString("SQL_DSN")
if sql_dsn == "" {
log.Fatal("SQL_DSN must be set in .env or environment")
}
db, err := gorm.Open(mysql.Open(sql_dsn), &gorm.Config{})
if err != nil {
log.Fatalf("failed to connect to MySQL Server: %v", err)
}
log.Println("connected to MySQL Server")
return db
}
func Init() *gorm.DB {
if shared_db_connection == nil {
shared_db_connection = initDB()
}
return shared_db_connection
}

View File

@@ -0,0 +1,124 @@
/**
* @file schema.rbac.go
* @brief RBAC schema models for GORM
*
* This file defines the base database models used by the RBAC
* (Role-Based Access Control) system. Models map to the following
* tables: user_types, users, auth_identities, auth_credentials,
* roles, permissions, role_permissions and user_roles.
*
* The structs include GORM tags for column names and relationships:
* - UserType: types of users.
* - User: main user record; links to UserType, AuthIdentity and UserRole.
* - AuthIdentity: external identity providers; links to AuthCredential.
* - AuthCredential: stored credentials for an identity.
* - Role: role definitions and their permissions and assigned users.
* - Permission: permission definitions.
* - RolePermission: join table between Role and Permission.
* - UserRole: join table between User and Role with optional expiration.
*
* These models are intended for use with GORM to perform ORM operations
* against the RBAC schema.
*/
package rbac
import (
"time"
)
type UserType struct {
ID uint `gorm:"primaryKey;column:id"`
Name string `gorm:"type:text;column:name"`
Description *string `gorm:"type:text;column:description"`
}
func (UserType) TableName() string { return "user_types" }
type User struct {
ID uint `gorm:"primaryKey;column:id"`
UserTypeID uint `gorm:"column:user_type"`
Name string `gorm:"type:text;column:name"`
LastName string `gorm:"type:text;column:last_name"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
UserType UserType `gorm:"foreignKey:UserTypeID;references:ID"`
AuthIdentities []AuthIdentity `gorm:"foreignKey:UserID;references:ID"`
UserRoles []UserRole `gorm:"foreignKey:UserID;references:ID"`
}
func (User) TableName() string { return "users" }
type AuthIdentity struct {
ID uint `gorm:"primaryKey;column:id"`
UserID uint `gorm:"column:user_id"`
Provider string `gorm:"type:text;column:provider"`
Identifier string `gorm:"type:text;column:identifier"`
IsPrimary bool `gorm:"column:is_primary"`
IsVerified bool `gorm:"column:is_verified"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
User User `gorm:"foreignKey:UserID;references:ID"`
Credentials []AuthCredential `gorm:"foreignKey:IdentityID;references:ID"`
}
func (AuthIdentity) TableName() string { return "auth_identities" }
type AuthCredential struct {
ID uint `gorm:"primaryKey;column:id"`
IdentityID uint `gorm:"column:identity_id"`
Password string `gorm:"type:text;column:password"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
Identity AuthIdentity `gorm:"foreignKey:IdentityID;references:ID"`
}
func (AuthCredential) TableName() string { return "auth_credentials" }
type Role struct {
ID uint `gorm:"primaryKey;column:id"`
Name string `gorm:"type:text;column:name"`
Description *string `gorm:"type:text;column:description"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
RolePermissions []RolePermission `gorm:"foreignKey:RoleID;references:ID"`
UserRoles []UserRole `gorm:"foreignKey:RoleID;references:ID"`
}
func (Role) TableName() string { return "roles" }
type Permission struct {
ID uint `gorm:"primaryKey;column:id"`
Name string `gorm:"type:text;column:name"`
Description *string `gorm:"type:text;column:description"`
RolePermissions []RolePermission `gorm:"foreignKey:PermissionID;references:ID"`
}
func (Permission) TableName() string { return "permissions" }
type RolePermission struct {
ID uint `gorm:"primaryKey;column:id"`
RoleID uint `gorm:"column:role_id"`
PermissionID uint `gorm:"column:permission_id"`
Role Role `gorm:"foreignKey:RoleID;references:ID"`
Permission Permission `gorm:"foreignKey:PermissionID;references:ID"`
}
func (RolePermission) TableName() string { return "role_permissions" }
type UserRole struct {
ID uint `gorm:"primaryKey;column:id"`
UserID uint `gorm:"column:user_id"`
RoleID uint `gorm:"column:role_id"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
ExpiresAt *time.Time `gorm:"column:expires_at"`
User User `gorm:"foreignKey:UserID;references:ID"`
Role Role `gorm:"foreignKey:RoleID;references:ID"`
}
func (UserRole) TableName() string { return "user_roles" }

34
app/main.go Normal file
View File

@@ -0,0 +1,34 @@
package main
import (
"fmt"
database "cloud.etaviaporte.com/api/libs/database"
rbac "cloud.etaviaporte.com/api/libs/database/schemas/rbac"
)
func main() {
var connection = database.Init()
// Prepare a variable to hold the fetched user record.
// Replace rbac.User with the actual struct name if different.
var user rbac.User
// Fetch the user whose primary key is 1.
// GORM's First method, when given a numeric id as the second argument, queries by primary key.
result := connection.First(&user, 1)
// result.Error holds any error from the DB operation (including "record not found").
// result.RowsAffected tells how many rows were returned.
if result.Error != nil {
// Handle the error (could be gorm.ErrRecordNotFound or a DB/connection error).
// For learning purposes we print the error and exit main.
fmt.Printf("failed to fetch user with id=1: %v\n", result.Error)
return
}
// If no error, user now contains the row from the database.
// Print the struct with field names and values to inspect the loaded data.
fmt.Printf("User fetched: %+v\n", user)
}

182
app/openapi.yaml Normal file
View File

@@ -0,0 +1,182 @@
openapi: 3.0.3
info:
title: Authorization API
version: "1.0.0"
description: Simple authorization endpoints for login, refresh, logout and getting current user info.
servers:
- url: http://localhost:8080
description: Local development server
paths:
/auth/login:
post:
summary: Obtain access and refresh tokens
tags:
- Auth
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
responses:
'200':
description: Tokens issued
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
/auth/refresh:
post:
summary: Refresh access token using a refresh token
tags:
- Auth
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RefreshRequest'
responses:
'200':
description: New tokens
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
/auth/logout:
post:
summary: Revoke refresh token / logout
tags:
- Auth
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RevokeRequest'
responses:
'204':
description: Successfully logged out (no content)
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
/auth/me:
get:
summary: Get current authenticated user
tags:
- Auth
security:
- bearerAuth: []
responses:
'200':
description: Current user profile
content:
application/json:
schema:
$ref: '#/components/schemas/UserProfile'
'401':
$ref: '#/components/responses/Unauthorized'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
LoginRequest:
type: object
required:
- username
- password
properties:
username:
type: string
example: user@example.com
password:
type: string
format: password
example: secret123
TokenResponse:
type: object
properties:
accessToken:
type: string
example: eyJhbGciOi...
refreshToken:
type: string
example: dummyr3fr3sht0k3n
expiresIn:
type: integer
description: Seconds until access token expiration
example: 3600
RefreshRequest:
type: object
required:
- refreshToken
properties:
refreshToken:
type: string
RevokeRequest:
type: object
required:
- refreshToken
properties:
refreshToken:
type: string
UserProfile:
type: object
properties:
id:
type: string
example: "123e4567-e89b-12d3-a456-426614174000"
username:
type: string
example: user@example.com
email:
type: string
example: user@example.com
Error:
type: object
properties:
code:
type: string
example: invalid_request
message:
type: string
example: "Detailed error message"
responses:
BadRequest:
description: Invalid request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: Authentication failed or missing credentials
content:
application/json:
schema:
$ref: '#/components/schemas/Error'