added some govno to postgres
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
SERVER_PORT=8080
|
SERVER_PORT=8080
|
||||||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/aegisguard?sslmode=disable
|
DATABASE_URL=postgres://postgres:postgres@localhost:5432/aegisguard?sslmode=disable
|
||||||
JWT_SECRET=bebebe_rewritemeeee_openssl_rand
|
JWT_SECRET=change_me_to_a_random_secret_at_least_32_chars
|
||||||
JWT_EXPIRATION=24h
|
JWT_EXPIRATION=24h
|
||||||
JWT_REFRESH_EXPIRATION=168h
|
JWT_REFRESH_EXPIRATION=168h
|
||||||
|
|||||||
+40
-4
@@ -12,8 +12,11 @@ import (
|
|||||||
docs "gitea.d3m0k1d.ru/HellreigN/Control-plane/docs"
|
docs "gitea.d3m0k1d.ru/HellreigN/Control-plane/docs"
|
||||||
"gitea.d3m0k1d.ru/HellreigN/Control-plane/internal/auth"
|
"gitea.d3m0k1d.ru/HellreigN/Control-plane/internal/auth"
|
||||||
"gitea.d3m0k1d.ru/HellreigN/Control-plane/internal/config"
|
"gitea.d3m0k1d.ru/HellreigN/Control-plane/internal/config"
|
||||||
|
"gitea.d3m0k1d.ru/HellreigN/Control-plane/internal/org"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/jackc/pgx/v5/stdlib"
|
||||||
|
"github.com/pressly/goose/v3"
|
||||||
"github.com/swaggo/files"
|
"github.com/swaggo/files"
|
||||||
"github.com/swaggo/gin-swagger"
|
"github.com/swaggo/gin-swagger"
|
||||||
)
|
)
|
||||||
@@ -48,16 +51,38 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Println("connected to postgres")
|
log.Println("connected to postgres")
|
||||||
|
|
||||||
repo := auth.NewRepository(pool)
|
db := stdlib.OpenDBFromPool(pool)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
if err := repo.Migrate(ctx); err != nil {
|
if err := goose.Up(db, "migrations"); err != nil {
|
||||||
log.Fatalf("failed to run migrations: %v", err)
|
log.Fatalf("failed to run migrations: %v", err)
|
||||||
}
|
}
|
||||||
log.Println("migrations applied")
|
log.Println("migrations applied")
|
||||||
|
|
||||||
|
repo := auth.NewRepository(pool)
|
||||||
|
orgRepo := org.NewRepository(pool)
|
||||||
|
|
||||||
svc := auth.NewService(repo, cfg.JWTSecret, cfg.JWTExpiration, cfg.JWTRefreshExpiration)
|
svc := auth.NewService(repo, cfg.JWTSecret, cfg.JWTExpiration, cfg.JWTRefreshExpiration)
|
||||||
handler := auth.NewHandler(svc)
|
handler := auth.NewHandler(svc)
|
||||||
|
|
||||||
|
orgSvc := org.NewService(orgRepo)
|
||||||
|
orgHandler := org.NewHandler(orgSvc)
|
||||||
|
|
||||||
|
loginLimiter := auth.NewRateLimiter(10, time.Minute)
|
||||||
|
authMW := auth.AuthMiddleware([]byte(cfg.JWTSecret))
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(30 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
if err := repo.DeleteExpiredRefreshTokens(cleanupCtx); err != nil {
|
||||||
|
log.Printf("failed to cleanup expired tokens: %v", err)
|
||||||
|
}
|
||||||
|
cleanupCancel()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(gin.Logger(), gin.Recovery())
|
r.Use(gin.Logger(), gin.Recovery())
|
||||||
@@ -74,10 +99,21 @@ func main() {
|
|||||||
api := r.Group("/api/auth")
|
api := r.Group("/api/auth")
|
||||||
{
|
{
|
||||||
api.POST("/register", handler.Register)
|
api.POST("/register", handler.Register)
|
||||||
api.POST("/login", handler.Login)
|
api.POST("/login", loginLimiter.Middleware(), handler.Login)
|
||||||
api.POST("/refresh", handler.Refresh)
|
api.POST("/refresh", handler.Refresh)
|
||||||
api.POST("/logout", handler.Logout)
|
api.POST("/logout", handler.Logout)
|
||||||
api.GET("/me", auth.AuthMiddleware([]byte(cfg.JWTSecret)), handler.Me)
|
api.GET("/me", authMW, handler.Me)
|
||||||
|
api.PUT("/me", authMW, handler.UpdateProfile)
|
||||||
|
api.PUT("/password", authMW, handler.ChangePassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
orgs := r.Group("/api/organizations", authMW)
|
||||||
|
{
|
||||||
|
orgs.POST("", orgHandler.Create)
|
||||||
|
orgs.GET("", orgHandler.List)
|
||||||
|
orgs.GET("/:id", orgHandler.GetByID)
|
||||||
|
orgs.PUT("/:id", orgHandler.Update)
|
||||||
|
orgs.DELETE("/:id", orgHandler.Delete)
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
|
|||||||
+480
-34
@@ -27,7 +27,7 @@ const docTemplate = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"auth"
|
"auth"
|
||||||
],
|
],
|
||||||
"summary": "Epta login",
|
"summary": "Login",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Login credentials",
|
"description": "Login credentials",
|
||||||
@@ -35,7 +35,7 @@ const docTemplate = `{
|
|||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.LoginRequest"
|
"$ref": "#/definitions/auth.LoginRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -43,19 +43,19 @@ const docTemplate = `{
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.AuthResponse"
|
"$ref": "#/definitions/auth.AuthResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Bad Request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"401": {
|
"401": {
|
||||||
"description": "Unauthorized",
|
"description": "Unauthorized",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ const docTemplate = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"auth"
|
"auth"
|
||||||
],
|
],
|
||||||
"summary": "Logout epta",
|
"summary": "Logout",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Refresh token to invalidate",
|
"description": "Refresh token to invalidate",
|
||||||
@@ -81,7 +81,7 @@ const docTemplate = `{
|
|||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.LogoutRequest"
|
"$ref": "#/definitions/auth.LogoutRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -98,13 +98,13 @@ const docTemplate = `{
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "Bad Request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"401": {
|
"401": {
|
||||||
"description": "Unauthorized",
|
"description": "Unauthorized",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,18 +127,121 @@ const docTemplate = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"auth"
|
"auth"
|
||||||
],
|
],
|
||||||
"summary": "Epta get current user",
|
"summary": "Get current user",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.UserResponse"
|
"$ref": "#/definitions/auth.UserResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"401": {
|
"401": {
|
||||||
"description": "Unauthorized",
|
"description": "Unauthorized",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Update current user's username",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Update profile",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Profile update",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.UpdateProfileRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.UserResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/auth/password": {
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Change current user's password",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Change password",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Password change details",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.PasswordChangeRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +259,7 @@ const docTemplate = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"auth"
|
"auth"
|
||||||
],
|
],
|
||||||
"summary": "Refresh epta token",
|
"summary": "Refresh token",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Refresh token",
|
"description": "Refresh token",
|
||||||
@@ -164,7 +267,7 @@ const docTemplate = `{
|
|||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.RefreshRequest"
|
"$ref": "#/definitions/auth.RefreshRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -172,19 +275,19 @@ const docTemplate = `{
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.AuthResponse"
|
"$ref": "#/definitions/auth.AuthResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Bad Request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"401": {
|
"401": {
|
||||||
"description": "Unauthorized",
|
"description": "Unauthorized",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,7 +305,7 @@ const docTemplate = `{
|
|||||||
"tags": [
|
"tags": [
|
||||||
"auth"
|
"auth"
|
||||||
],
|
],
|
||||||
"summary": "Epta registration",
|
"summary": "Register",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Registration details",
|
"description": "Registration details",
|
||||||
@@ -210,7 +313,7 @@ const docTemplate = `{
|
|||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.RegisterRequest"
|
"$ref": "#/definitions/auth.RegisterRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -218,19 +321,245 @@ const docTemplate = `{
|
|||||||
"201": {
|
"201": {
|
||||||
"description": "Created",
|
"description": "Created",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.UserResponse"
|
"$ref": "#/definitions/auth.AuthResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Bad Request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"409": {
|
"409": {
|
||||||
"description": "Conflict",
|
"description": "Conflict",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/organizations": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get all organizations",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"organizations"
|
||||||
|
],
|
||||||
|
"summary": "List organizations",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.OrgListResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Create a new organization",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"organizations"
|
||||||
|
],
|
||||||
|
"summary": "Create organization",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Organization details",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.CreateOrgRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.OrgResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"409": {
|
||||||
|
"description": "Conflict",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/organizations/{id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get organization details",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"organizations"
|
||||||
|
],
|
||||||
|
"summary": "Get organization by ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.OrgResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Update organization name",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"organizations"
|
||||||
|
],
|
||||||
|
"summary": "Update organization",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "New organization details",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.UpdateOrgRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.OrgResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Delete an organization",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"organizations"
|
||||||
|
],
|
||||||
|
"summary": "Delete organization",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,7 +567,7 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"internal_auth.AuthResponse": {
|
"auth.AuthResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"refresh_token": {
|
"refresh_token": {
|
||||||
@@ -250,11 +579,11 @@ const docTemplate = `{
|
|||||||
"example": "eyJhbGciOiJIUzI1NiIs..."
|
"example": "eyJhbGciOiJIUzI1NiIs..."
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"$ref": "#/definitions/internal_auth.UserPublic"
|
"$ref": "#/definitions/auth.UserPublic"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"internal_auth.ErrorResponse": {
|
"auth.ErrorResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"error": {
|
"error": {
|
||||||
@@ -263,7 +592,7 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"internal_auth.LoginRequest": {
|
"auth.LoginRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"email",
|
"email",
|
||||||
@@ -280,7 +609,7 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"internal_auth.LogoutRequest": {
|
"auth.LogoutRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"refresh_token"
|
"refresh_token"
|
||||||
@@ -292,7 +621,25 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"internal_auth.RefreshRequest": {
|
"auth.PasswordChangeRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"new_password",
|
||||||
|
"old_password"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"new_password": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 8,
|
||||||
|
"example": "NewSecret456!"
|
||||||
|
},
|
||||||
|
"old_password": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Secret123!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth.RefreshRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"refresh_token"
|
"refresh_token"
|
||||||
@@ -304,7 +651,7 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"internal_auth.RegisterRequest": {
|
"auth.RegisterRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"email",
|
"email",
|
||||||
@@ -318,8 +665,8 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 6,
|
"minLength": 8,
|
||||||
"example": "secret123"
|
"example": "Secret123!"
|
||||||
},
|
},
|
||||||
"username": {
|
"username": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -329,7 +676,21 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"internal_auth.UserPublic": {
|
"auth.UpdateProfileRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"username"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"username": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 30,
|
||||||
|
"minLength": 3,
|
||||||
|
"example": "john_updated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth.UserPublic": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"created_at": {
|
"created_at": {
|
||||||
@@ -346,11 +707,96 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"internal_auth.UserResponse": {
|
"auth.UserResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"user": {
|
"user": {
|
||||||
"$ref": "#/definitions/internal_auth.UserPublic"
|
"$ref": "#/definitions/auth.UserPublic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"org.CreateOrgRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"slug"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 100,
|
||||||
|
"minLength": 2,
|
||||||
|
"example": "My Corp"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 50,
|
||||||
|
"minLength": 2,
|
||||||
|
"example": "my-corp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"org.ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"org.OrgListResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"organizations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/org.Organization"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"org.OrgResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"organization": {
|
||||||
|
"$ref": "#/definitions/org.Organization"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"org.Organization": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"org.UpdateOrgRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 100,
|
||||||
|
"minLength": 2,
|
||||||
|
"example": "My Corp Updated"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+480
-34
@@ -22,7 +22,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"auth"
|
"auth"
|
||||||
],
|
],
|
||||||
"summary": "Epta login",
|
"summary": "Login",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Login credentials",
|
"description": "Login credentials",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.LoginRequest"
|
"$ref": "#/definitions/auth.LoginRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -38,19 +38,19 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.AuthResponse"
|
"$ref": "#/definitions/auth.AuthResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Bad Request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"401": {
|
"401": {
|
||||||
"description": "Unauthorized",
|
"description": "Unauthorized",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"auth"
|
"auth"
|
||||||
],
|
],
|
||||||
"summary": "Logout epta",
|
"summary": "Logout",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Refresh token to invalidate",
|
"description": "Refresh token to invalidate",
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.LogoutRequest"
|
"$ref": "#/definitions/auth.LogoutRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -93,13 +93,13 @@
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "Bad Request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"401": {
|
"401": {
|
||||||
"description": "Unauthorized",
|
"description": "Unauthorized",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,18 +122,121 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"auth"
|
"auth"
|
||||||
],
|
],
|
||||||
"summary": "Epta get current user",
|
"summary": "Get current user",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.UserResponse"
|
"$ref": "#/definitions/auth.UserResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"401": {
|
"401": {
|
||||||
"description": "Unauthorized",
|
"description": "Unauthorized",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Update current user's username",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Update profile",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Profile update",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.UpdateProfileRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.UserResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/auth/password": {
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Change current user's password",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Change password",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Password change details",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.PasswordChangeRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +254,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"auth"
|
"auth"
|
||||||
],
|
],
|
||||||
"summary": "Refresh epta token",
|
"summary": "Refresh token",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Refresh token",
|
"description": "Refresh token",
|
||||||
@@ -159,7 +262,7 @@
|
|||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.RefreshRequest"
|
"$ref": "#/definitions/auth.RefreshRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -167,19 +270,19 @@
|
|||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.AuthResponse"
|
"$ref": "#/definitions/auth.AuthResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Bad Request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"401": {
|
"401": {
|
||||||
"description": "Unauthorized",
|
"description": "Unauthorized",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,7 +300,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"auth"
|
"auth"
|
||||||
],
|
],
|
||||||
"summary": "Epta registration",
|
"summary": "Register",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "Registration details",
|
"description": "Registration details",
|
||||||
@@ -205,7 +308,7 @@
|
|||||||
"in": "body",
|
"in": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.RegisterRequest"
|
"$ref": "#/definitions/auth.RegisterRequest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -213,19 +316,245 @@
|
|||||||
"201": {
|
"201": {
|
||||||
"description": "Created",
|
"description": "Created",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.UserResponse"
|
"$ref": "#/definitions/auth.AuthResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Bad Request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"409": {
|
"409": {
|
||||||
"description": "Conflict",
|
"description": "Conflict",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/internal_auth.ErrorResponse"
|
"$ref": "#/definitions/auth.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/organizations": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get all organizations",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"organizations"
|
||||||
|
],
|
||||||
|
"summary": "List organizations",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.OrgListResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Create a new organization",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"organizations"
|
||||||
|
],
|
||||||
|
"summary": "Create organization",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Organization details",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.CreateOrgRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.OrgResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"409": {
|
||||||
|
"description": "Conflict",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/organizations/{id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get organization details",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"organizations"
|
||||||
|
],
|
||||||
|
"summary": "Get organization by ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.OrgResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Update organization name",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"organizations"
|
||||||
|
],
|
||||||
|
"summary": "Update organization",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "New organization details",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.UpdateOrgRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.OrgResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Delete an organization",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"organizations"
|
||||||
|
],
|
||||||
|
"summary": "Delete organization",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Organization ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/org.ErrorResponse"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,7 +562,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"internal_auth.AuthResponse": {
|
"auth.AuthResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"refresh_token": {
|
"refresh_token": {
|
||||||
@@ -245,11 +574,11 @@
|
|||||||
"example": "eyJhbGciOiJIUzI1NiIs..."
|
"example": "eyJhbGciOiJIUzI1NiIs..."
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"$ref": "#/definitions/internal_auth.UserPublic"
|
"$ref": "#/definitions/auth.UserPublic"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"internal_auth.ErrorResponse": {
|
"auth.ErrorResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"error": {
|
"error": {
|
||||||
@@ -258,7 +587,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"internal_auth.LoginRequest": {
|
"auth.LoginRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"email",
|
"email",
|
||||||
@@ -275,7 +604,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"internal_auth.LogoutRequest": {
|
"auth.LogoutRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"refresh_token"
|
"refresh_token"
|
||||||
@@ -287,7 +616,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"internal_auth.RefreshRequest": {
|
"auth.PasswordChangeRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"new_password",
|
||||||
|
"old_password"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"new_password": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 8,
|
||||||
|
"example": "NewSecret456!"
|
||||||
|
},
|
||||||
|
"old_password": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Secret123!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth.RefreshRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"refresh_token"
|
"refresh_token"
|
||||||
@@ -299,7 +646,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"internal_auth.RegisterRequest": {
|
"auth.RegisterRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"email",
|
"email",
|
||||||
@@ -313,8 +660,8 @@
|
|||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 6,
|
"minLength": 8,
|
||||||
"example": "secret123"
|
"example": "Secret123!"
|
||||||
},
|
},
|
||||||
"username": {
|
"username": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -324,7 +671,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"internal_auth.UserPublic": {
|
"auth.UpdateProfileRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"username"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"username": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 30,
|
||||||
|
"minLength": 3,
|
||||||
|
"example": "john_updated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth.UserPublic": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"created_at": {
|
"created_at": {
|
||||||
@@ -341,11 +702,96 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"internal_auth.UserResponse": {
|
"auth.UserResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"user": {
|
"user": {
|
||||||
"$ref": "#/definitions/internal_auth.UserPublic"
|
"$ref": "#/definitions/auth.UserPublic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"org.CreateOrgRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"slug"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 100,
|
||||||
|
"minLength": 2,
|
||||||
|
"example": "My Corp"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 50,
|
||||||
|
"minLength": 2,
|
||||||
|
"example": "my-corp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"org.ErrorResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"org.OrgListResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"organizations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/org.Organization"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"org.OrgResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"organization": {
|
||||||
|
"$ref": "#/definitions/org.Organization"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"org.Organization": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"org.UpdateOrgRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 100,
|
||||||
|
"minLength": 2,
|
||||||
|
"example": "My Corp Updated"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+323
-34
@@ -1,5 +1,5 @@
|
|||||||
definitions:
|
definitions:
|
||||||
internal_auth.AuthResponse:
|
auth.AuthResponse:
|
||||||
properties:
|
properties:
|
||||||
refresh_token:
|
refresh_token:
|
||||||
example: dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4=
|
example: dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4=
|
||||||
@@ -8,15 +8,15 @@ definitions:
|
|||||||
example: eyJhbGciOiJIUzI1NiIs...
|
example: eyJhbGciOiJIUzI1NiIs...
|
||||||
type: string
|
type: string
|
||||||
user:
|
user:
|
||||||
$ref: '#/definitions/internal_auth.UserPublic'
|
$ref: '#/definitions/auth.UserPublic'
|
||||||
type: object
|
type: object
|
||||||
internal_auth.ErrorResponse:
|
auth.ErrorResponse:
|
||||||
properties:
|
properties:
|
||||||
error:
|
error:
|
||||||
example: invalid email or password
|
example: invalid email or password
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
internal_auth.LoginRequest:
|
auth.LoginRequest:
|
||||||
properties:
|
properties:
|
||||||
email:
|
email:
|
||||||
example: john@example.com
|
example: john@example.com
|
||||||
@@ -28,7 +28,7 @@ definitions:
|
|||||||
- email
|
- email
|
||||||
- password
|
- password
|
||||||
type: object
|
type: object
|
||||||
internal_auth.LogoutRequest:
|
auth.LogoutRequest:
|
||||||
properties:
|
properties:
|
||||||
refresh_token:
|
refresh_token:
|
||||||
example: dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4=
|
example: dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4=
|
||||||
@@ -36,7 +36,20 @@ definitions:
|
|||||||
required:
|
required:
|
||||||
- refresh_token
|
- refresh_token
|
||||||
type: object
|
type: object
|
||||||
internal_auth.RefreshRequest:
|
auth.PasswordChangeRequest:
|
||||||
|
properties:
|
||||||
|
new_password:
|
||||||
|
example: NewSecret456!
|
||||||
|
minLength: 8
|
||||||
|
type: string
|
||||||
|
old_password:
|
||||||
|
example: Secret123!
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- new_password
|
||||||
|
- old_password
|
||||||
|
type: object
|
||||||
|
auth.RefreshRequest:
|
||||||
properties:
|
properties:
|
||||||
refresh_token:
|
refresh_token:
|
||||||
example: dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4=
|
example: dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4=
|
||||||
@@ -44,14 +57,14 @@ definitions:
|
|||||||
required:
|
required:
|
||||||
- refresh_token
|
- refresh_token
|
||||||
type: object
|
type: object
|
||||||
internal_auth.RegisterRequest:
|
auth.RegisterRequest:
|
||||||
properties:
|
properties:
|
||||||
email:
|
email:
|
||||||
example: john@example.com
|
example: john@example.com
|
||||||
type: string
|
type: string
|
||||||
password:
|
password:
|
||||||
example: secret123
|
example: Secret123!
|
||||||
minLength: 6
|
minLength: 8
|
||||||
type: string
|
type: string
|
||||||
username:
|
username:
|
||||||
example: john
|
example: john
|
||||||
@@ -63,7 +76,17 @@ definitions:
|
|||||||
- password
|
- password
|
||||||
- username
|
- username
|
||||||
type: object
|
type: object
|
||||||
internal_auth.UserPublic:
|
auth.UpdateProfileRequest:
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
example: john_updated
|
||||||
|
maxLength: 30
|
||||||
|
minLength: 3
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- username
|
||||||
|
type: object
|
||||||
|
auth.UserPublic:
|
||||||
properties:
|
properties:
|
||||||
created_at:
|
created_at:
|
||||||
type: string
|
type: string
|
||||||
@@ -74,10 +97,68 @@ definitions:
|
|||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
internal_auth.UserResponse:
|
auth.UserResponse:
|
||||||
properties:
|
properties:
|
||||||
user:
|
user:
|
||||||
$ref: '#/definitions/internal_auth.UserPublic'
|
$ref: '#/definitions/auth.UserPublic'
|
||||||
|
type: object
|
||||||
|
org.CreateOrgRequest:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
example: My Corp
|
||||||
|
maxLength: 100
|
||||||
|
minLength: 2
|
||||||
|
type: string
|
||||||
|
slug:
|
||||||
|
example: my-corp
|
||||||
|
maxLength: 50
|
||||||
|
minLength: 2
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- slug
|
||||||
|
type: object
|
||||||
|
org.ErrorResponse:
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
org.OrgListResponse:
|
||||||
|
properties:
|
||||||
|
organizations:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/org.Organization'
|
||||||
|
type: array
|
||||||
|
total:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
org.OrgResponse:
|
||||||
|
properties:
|
||||||
|
organization:
|
||||||
|
$ref: '#/definitions/org.Organization'
|
||||||
|
type: object
|
||||||
|
org.Organization:
|
||||||
|
properties:
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
slug:
|
||||||
|
type: string
|
||||||
|
updated_at:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
org.UpdateOrgRequest:
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
example: My Corp Updated
|
||||||
|
maxLength: 100
|
||||||
|
minLength: 2
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
type: object
|
type: object
|
||||||
info:
|
info:
|
||||||
contact: {}
|
contact: {}
|
||||||
@@ -96,23 +177,23 @@ paths:
|
|||||||
name: request
|
name: request
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.LoginRequest'
|
$ref: '#/definitions/auth.LoginRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.AuthResponse'
|
$ref: '#/definitions/auth.AuthResponse'
|
||||||
"400":
|
"400":
|
||||||
description: Bad Request
|
description: Bad Request
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.ErrorResponse'
|
$ref: '#/definitions/auth.ErrorResponse'
|
||||||
"401":
|
"401":
|
||||||
description: Unauthorized
|
description: Unauthorized
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.ErrorResponse'
|
$ref: '#/definitions/auth.ErrorResponse'
|
||||||
summary: Epta login
|
summary: Login
|
||||||
tags:
|
tags:
|
||||||
- auth
|
- auth
|
||||||
/api/auth/logout:
|
/api/auth/logout:
|
||||||
@@ -126,7 +207,7 @@ paths:
|
|||||||
name: request
|
name: request
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.LogoutRequest'
|
$ref: '#/definitions/auth.LogoutRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
@@ -139,12 +220,12 @@ paths:
|
|||||||
"400":
|
"400":
|
||||||
description: Bad Request
|
description: Bad Request
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.ErrorResponse'
|
$ref: '#/definitions/auth.ErrorResponse'
|
||||||
"401":
|
"401":
|
||||||
description: Unauthorized
|
description: Unauthorized
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.ErrorResponse'
|
$ref: '#/definitions/auth.ErrorResponse'
|
||||||
summary: Logout epta
|
summary: Logout
|
||||||
tags:
|
tags:
|
||||||
- auth
|
- auth
|
||||||
/api/auth/me:
|
/api/auth/me:
|
||||||
@@ -158,14 +239,79 @@ paths:
|
|||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.UserResponse'
|
$ref: '#/definitions/auth.UserResponse'
|
||||||
"401":
|
"401":
|
||||||
description: Unauthorized
|
description: Unauthorized
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.ErrorResponse'
|
$ref: '#/definitions/auth.ErrorResponse'
|
||||||
security:
|
security:
|
||||||
- Bearer: []
|
- Bearer: []
|
||||||
summary: Epta get current user
|
summary: Get current user
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Update current user's username
|
||||||
|
parameters:
|
||||||
|
- description: Profile update
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/auth.UpdateProfileRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/auth.UserResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/auth.ErrorResponse'
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/auth.ErrorResponse'
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
summary: Update profile
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
|
/api/auth/password:
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Change current user's password
|
||||||
|
parameters:
|
||||||
|
- description: Password change details
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/auth.PasswordChangeRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/auth.ErrorResponse'
|
||||||
|
"401":
|
||||||
|
description: Unauthorized
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/auth.ErrorResponse'
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
summary: Change password
|
||||||
tags:
|
tags:
|
||||||
- auth
|
- auth
|
||||||
/api/auth/refresh:
|
/api/auth/refresh:
|
||||||
@@ -179,23 +325,23 @@ paths:
|
|||||||
name: request
|
name: request
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.RefreshRequest'
|
$ref: '#/definitions/auth.RefreshRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.AuthResponse'
|
$ref: '#/definitions/auth.AuthResponse'
|
||||||
"400":
|
"400":
|
||||||
description: Bad Request
|
description: Bad Request
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.ErrorResponse'
|
$ref: '#/definitions/auth.ErrorResponse'
|
||||||
"401":
|
"401":
|
||||||
description: Unauthorized
|
description: Unauthorized
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.ErrorResponse'
|
$ref: '#/definitions/auth.ErrorResponse'
|
||||||
summary: Refresh epta token
|
summary: Refresh token
|
||||||
tags:
|
tags:
|
||||||
- auth
|
- auth
|
||||||
/api/auth/register:
|
/api/auth/register:
|
||||||
@@ -209,25 +355,168 @@ paths:
|
|||||||
name: request
|
name: request
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.RegisterRequest'
|
$ref: '#/definitions/auth.RegisterRequest'
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"201":
|
"201":
|
||||||
description: Created
|
description: Created
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.UserResponse'
|
$ref: '#/definitions/auth.AuthResponse'
|
||||||
"400":
|
"400":
|
||||||
description: Bad Request
|
description: Bad Request
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.ErrorResponse'
|
$ref: '#/definitions/auth.ErrorResponse'
|
||||||
"409":
|
"409":
|
||||||
description: Conflict
|
description: Conflict
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/internal_auth.ErrorResponse'
|
$ref: '#/definitions/auth.ErrorResponse'
|
||||||
summary: Epta registration
|
summary: Register
|
||||||
tags:
|
tags:
|
||||||
- auth
|
- auth
|
||||||
|
/api/organizations:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Get all organizations
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/org.OrgListResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/org.ErrorResponse'
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
summary: List organizations
|
||||||
|
tags:
|
||||||
|
- organizations
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Create a new organization
|
||||||
|
parameters:
|
||||||
|
- description: Organization details
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/org.CreateOrgRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"201":
|
||||||
|
description: Created
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/org.OrgResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/org.ErrorResponse'
|
||||||
|
"409":
|
||||||
|
description: Conflict
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/org.ErrorResponse'
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
summary: Create organization
|
||||||
|
tags:
|
||||||
|
- organizations
|
||||||
|
/api/organizations/{id}:
|
||||||
|
delete:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Delete an organization
|
||||||
|
parameters:
|
||||||
|
- description: Organization ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/org.ErrorResponse'
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
summary: Delete organization
|
||||||
|
tags:
|
||||||
|
- organizations
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Get organization details
|
||||||
|
parameters:
|
||||||
|
- description: Organization ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/org.OrgResponse'
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/org.ErrorResponse'
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
summary: Get organization by ID
|
||||||
|
tags:
|
||||||
|
- organizations
|
||||||
|
put:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Update organization name
|
||||||
|
parameters:
|
||||||
|
- description: Organization ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: New organization details
|
||||||
|
in: body
|
||||||
|
name: request
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/org.UpdateOrgRequest'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/org.OrgResponse'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/org.ErrorResponse'
|
||||||
|
"404":
|
||||||
|
description: Not Found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/org.ErrorResponse'
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
summary: Update organization
|
||||||
|
tags:
|
||||||
|
- organizations
|
||||||
schemes:
|
schemes:
|
||||||
- http
|
- http
|
||||||
securityDefinitions:
|
securityDefinitions:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgx/v5 v5.7.4
|
github.com/jackc/pgx/v5 v5.7.4
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/pressly/goose/v3 v3.24.2
|
||||||
github.com/swaggo/files v1.0.1
|
github.com/swaggo/files v1.0.1
|
||||||
github.com/swaggo/gin-swagger v1.6.1
|
github.com/swaggo/gin-swagger v1.6.1
|
||||||
github.com/swaggo/swag v1.16.6
|
github.com/swaggo/swag v1.16.6
|
||||||
@@ -42,14 +43,17 @@ require (
|
|||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.6 // indirect
|
github.com/mailru/easyjson v0.7.6 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||||
|
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
|
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/arch v0.28.0 // indirect
|
golang.org/x/arch v0.28.0 // indirect
|
||||||
golang.org/x/mod v0.36.0 // indirect
|
golang.org/x/mod v0.36.0 // indirect
|
||||||
golang.org/x/net v0.56.0 // indirect
|
golang.org/x/net v0.56.0 // indirect
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||||
@@ -84,22 +86,32 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA
|
|||||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
|
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||||
|
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
||||||
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.3.1/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pressly/goose/v3 v3.24.2 h1:c/ie0Gm8rnIVKvnDQ/scHErv46jrDv9b4I0WRcFJzYU=
|
||||||
|
github.com/pressly/goose/v3 v3.24.2/go.mod h1:kjefwFB0eR4w30Td2Gj2Mznyw94vSP+2jJYkOVNbD1k=
|
||||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||||
|
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
@@ -128,12 +140,16 @@ go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF
|
|||||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
golang.org/x/arch v0.28.0 h1:wVwVdqsTuUbJvhYVCspQYwZXHNYeLSoZnmHD+ggddpQ=
|
golang.org/x/arch v0.28.0 h1:wVwVdqsTuUbJvhYVCspQYwZXHNYeLSoZnmHD+ggddpQ=
|
||||||
golang.org/x/arch v0.28.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
golang.org/x/arch v0.28.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||||
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||||
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||||
@@ -187,3 +203,11 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
|
||||||
|
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
|
||||||
|
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/sqlite v1.36.2 h1:vjcSazuoFve9Wm0IVNHgmJECoOXLZM1KfMXbcX2axHA=
|
||||||
|
modernc.org/sqlite v1.36.2/go.mod h1:ADySlx7K4FdY5MaJcEv86hTJ0PjedAloTUuif0YS3ws=
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# JWT Аутентификация — AegisGuard API
|
||||||
|
|
||||||
|
## Схема работы
|
||||||
|
|
||||||
|
- **access_token** — JWT, живёт 24 часа. Передаётся в заголовке `Authorization: Bearer`.
|
||||||
|
- **refresh_token** — случайная строка, хранится в БД в виде хеша. Используется **один раз** (ротация): при запросе новой пары старый токен удаляется.
|
||||||
|
- Регистрация сразу возвращает токены — отдельный логин не нужен.
|
||||||
|
|
||||||
|
## Эндпоинты
|
||||||
|
|
||||||
|
### POST /api/auth/register
|
||||||
|
|
||||||
|
Создание аккаунта.
|
||||||
|
|
||||||
|
```
|
||||||
|
Запрос:
|
||||||
|
{ "username": "john", "email": "john@example.com", "password": "Secret123" }
|
||||||
|
|
||||||
|
Ответ 201:
|
||||||
|
{
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
|
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4=",
|
||||||
|
"user": {
|
||||||
|
"id": "uuid",
|
||||||
|
"username": "john",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"created_at": "2026-06-13T12:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `username` — 3–30 символов
|
||||||
|
- `email` — валидный email
|
||||||
|
- `password` — минимум 8 символов, обязательно заглавная + строчная + цифра
|
||||||
|
|
||||||
|
Ошибки: `400` (валидация), `409` (email уже занят).
|
||||||
|
|
||||||
|
### POST /api/auth/login
|
||||||
|
|
||||||
|
```
|
||||||
|
Запрос:
|
||||||
|
{ "email": "john@example.com", "password": "Secret123" }
|
||||||
|
|
||||||
|
Ответ 200:
|
||||||
|
{ "token": "...", "refresh_token": "...", "user": { ... } }
|
||||||
|
```
|
||||||
|
|
||||||
|
Rate limit: 10 попыток в минуту с одного IP (`429 Too Many Requests`).
|
||||||
|
|
||||||
|
### POST /api/auth/refresh
|
||||||
|
|
||||||
|
Обновить токены по refresh_token. Старый удаляется, выдаётся новая пара.
|
||||||
|
|
||||||
|
```
|
||||||
|
Запрос:
|
||||||
|
{ "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4=" }
|
||||||
|
|
||||||
|
Ответ 200:
|
||||||
|
{ "token": "...", "refresh_token": "...", "user": { ... } }
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/auth/logout
|
||||||
|
|
||||||
|
Удалить refresh_token из БД.
|
||||||
|
|
||||||
|
```
|
||||||
|
Запрос:
|
||||||
|
{ "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4=" }
|
||||||
|
|
||||||
|
Ответ 200:
|
||||||
|
{ "message": "logged out successfully" }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Заголовок авторизации
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Формат JWT
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"user_id": "uuid",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"sub": "uuid",
|
||||||
|
"exp": 1718000000,
|
||||||
|
"iat": 1717913600
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `user_id` — UUID пользователя
|
||||||
|
- `email` — Email пользователя
|
||||||
|
- `sub` — то же, что `user_id`
|
||||||
|
- `exp` — Unix-timestamp истечения токена
|
||||||
|
- `iat` — Unix-timestamp выпуска токена
|
||||||
|
|
||||||
|
## Формат ошибок
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "error": "описание" }
|
||||||
|
```
|
||||||
|
|
||||||
|
- `400` — ошибка валидации
|
||||||
|
- `401` — неверный email/пароль, токен протух или невалиден
|
||||||
|
- `409` — email уже зарегистрирован
|
||||||
|
- `429` — превышен лимит попыток логина
|
||||||
|
- `500` — внутренняя ошибка сервера
|
||||||
+101
-6
@@ -16,13 +16,13 @@ func NewHandler(service *Service) *Handler {
|
|||||||
return &Handler{service: service}
|
return &Handler{service: service}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Epta registration
|
// @Summary Register epta
|
||||||
// @Description Create user account with username, email, password
|
// @Description Create user account with username, email, password
|
||||||
// @Tags auth
|
// @Tags auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param request body RegisterRequest true "Registration details"
|
// @Param request body RegisterRequest true "Registration details"
|
||||||
// @Success 201 {object} UserResponse
|
// @Success 201 {object} AuthResponse
|
||||||
// @Failure 400 {object} ErrorResponse
|
// @Failure 400 {object} ErrorResponse
|
||||||
// @Failure 409 {object} ErrorResponse
|
// @Failure 409 {object} ErrorResponse
|
||||||
// @Router /api/auth/register [post]
|
// @Router /api/auth/register [post]
|
||||||
@@ -33,21 +33,25 @@ func (h *Handler) Register(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.service.Register(c.Request.Context(), req)
|
resp, err := h.service.Register(c.Request.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrEmailExists) {
|
if errors.Is(err, ErrEmailExists) {
|
||||||
c.JSON(http.StatusConflict, ErrorResponse{Error: err.Error()})
|
c.JSON(http.StatusConflict, ErrorResponse{Error: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, ErrWeakPassword) {
|
||||||
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
log.Printf("register error: %v", err)
|
log.Printf("register error: %v", err)
|
||||||
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"})
|
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, UserResponse{User: *user})
|
c.JSON(http.StatusCreated, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Epta login
|
// @Summary Login
|
||||||
// @Description Authenticate user with email and password, returns JWT token
|
// @Description Authenticate user with email and password, returns JWT token
|
||||||
// @Tags auth
|
// @Tags auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -139,7 +143,7 @@ func (h *Handler) Logout(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"message": "logged out successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": "logged out successfully"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Epta get current user
|
// @Summary Get epta current user
|
||||||
// @Description Get authenticated user's profile
|
// @Description Get authenticated user's profile
|
||||||
// @Tags auth
|
// @Tags auth
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -174,3 +178,94 @@ func (h *Handler) Me(c *gin.Context) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, UserResponse{User: *user})
|
c.JSON(http.StatusOK, UserResponse{User: *user})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary Change epta password
|
||||||
|
// @Description Change current user's password
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param request body PasswordChangeRequest true "Password change details"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Failure 400 {object} ErrorResponse
|
||||||
|
// @Failure 401 {object} ErrorResponse
|
||||||
|
// @Router /api/auth/password [put]
|
||||||
|
func (h *Handler) ChangePassword(c *gin.Context) {
|
||||||
|
rawUserID, exists := c.Get("user_id")
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusUnauthorized, ErrorResponse{Error: "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, ok := rawUserID.(string)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "invalid user ID in context"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req PasswordChangeRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.ChangePassword(c.Request.Context(), userID, req); err != nil {
|
||||||
|
if errors.Is(err, ErrWrongPassword) || errors.Is(err, ErrSamePassword) || errors.Is(err, ErrWeakPassword) {
|
||||||
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.Is(err, ErrUserNotFound) || errors.Is(err, ErrInvalidUserID) {
|
||||||
|
c.JSON(http.StatusNotFound, ErrorResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("change password error: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "password changed successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Update profile
|
||||||
|
// @Description Update current user's username
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param request body UpdateProfileRequest true "Profile update"
|
||||||
|
// @Success 200 {object} UserResponse
|
||||||
|
// @Failure 400 {object} ErrorResponse
|
||||||
|
// @Failure 401 {object} ErrorResponse
|
||||||
|
// @Router /api/auth/me [put]
|
||||||
|
func (h *Handler) UpdateProfile(c *gin.Context) {
|
||||||
|
rawUserID, exists := c.Get("user_id")
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusUnauthorized, ErrorResponse{Error: "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, ok := rawUserID.(string)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "invalid user ID in context"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req UpdateProfileRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.service.UpdateProfile(c.Request.Context(), userID, req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrUserNotFound) || errors.Is(err, ErrInvalidUserID) {
|
||||||
|
c.JSON(http.StatusNotFound, ErrorResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("update profile error: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, UserResponse{User: *user})
|
||||||
|
}
|
||||||
|
|||||||
+10
-1
@@ -15,7 +15,7 @@ type User struct {
|
|||||||
type RegisterRequest struct {
|
type RegisterRequest struct {
|
||||||
Username string `json:"username" binding:"required,min=3,max=30" example:"john"`
|
Username string `json:"username" binding:"required,min=3,max=30" example:"john"`
|
||||||
Email string `json:"email" binding:"required,email" example:"john@example.com"`
|
Email string `json:"email" binding:"required,email" example:"john@example.com"`
|
||||||
Password string `json:"password" binding:"required,min=6" example:"secret123"`
|
Password string `json:"password" binding:"required,min=8" example:"Secret123!"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
@@ -65,6 +65,15 @@ type UserResponse struct {
|
|||||||
User UserPublic `json:"user"`
|
User UserPublic `json:"user"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PasswordChangeRequest struct {
|
||||||
|
OldPassword string `json:"old_password" binding:"required" example:"Secret123!"`
|
||||||
|
NewPassword string `json:"new_password" binding:"required,min=8" example:"NewSecret456!"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateProfileRequest struct {
|
||||||
|
Username string `json:"username" binding:"required,min=3,max=30" example:"john_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
type ErrorResponse struct {
|
type ErrorResponse struct {
|
||||||
Error string `json:"error" example:"invalid email or password"`
|
Error string `json:"error" example:"invalid email or password"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type visitor struct {
|
||||||
|
count int
|
||||||
|
lastSeen time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type RateLimiter struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
visitors map[string]*visitor
|
||||||
|
rate int
|
||||||
|
window time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRateLimiter(rate int, window time.Duration) *RateLimiter {
|
||||||
|
rl := &RateLimiter{
|
||||||
|
visitors: make(map[string]*visitor),
|
||||||
|
rate: rate,
|
||||||
|
window: window,
|
||||||
|
}
|
||||||
|
|
||||||
|
go rl.cleanup()
|
||||||
|
return rl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) cleanup() {
|
||||||
|
ticker := time.NewTicker(10 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
rl.mu.Lock()
|
||||||
|
now := time.Now()
|
||||||
|
for ip, v := range rl.visitors {
|
||||||
|
if now.Sub(v.lastSeen) > rl.window*2 {
|
||||||
|
delete(rl.visitors, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rl.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) Middleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
ip := c.ClientIP()
|
||||||
|
|
||||||
|
rl.mu.Lock()
|
||||||
|
v, exists := rl.visitors[ip]
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
if !exists || now.Sub(v.lastSeen) > rl.window {
|
||||||
|
rl.visitors[ip] = &visitor{count: 1, lastSeen: now}
|
||||||
|
rl.mu.Unlock()
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
v.count++
|
||||||
|
v.lastSeen = now
|
||||||
|
|
||||||
|
if v.count > rl.rate {
|
||||||
|
rl.mu.Unlock()
|
||||||
|
c.JSON(http.StatusTooManyRequests, ErrorResponse{Error: "too many requests, try again later"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.mu.Unlock()
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-29
@@ -17,30 +17,6 @@ func NewRepository(pool *pgxpool.Pool) *Repository {
|
|||||||
return &Repository{pool: pool}
|
return &Repository{pool: pool}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) Migrate(ctx context.Context) error {
|
|
||||||
schema := `
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
username TEXT NOT NULL,
|
|
||||||
email TEXT NOT NULL UNIQUE,
|
|
||||||
password_hash TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
token_hash TEXT NOT NULL UNIQUE,
|
|
||||||
expires_at TIMESTAMPTZ NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires_at ON refresh_tokens(expires_at);
|
|
||||||
`
|
|
||||||
_, err := r.pool.Exec(ctx, schema)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repository) CreateUser(ctx context.Context, user *User) error {
|
func (r *Repository) CreateUser(ctx context.Context, user *User) error {
|
||||||
user.ID = uuid.New().String()
|
user.ID = uuid.New().String()
|
||||||
user.CreatedAt = time.Now().UTC()
|
user.CreatedAt = time.Now().UTC()
|
||||||
@@ -86,7 +62,7 @@ func (r *Repository) CreateRefreshToken(ctx context.Context, doc *RefreshTokenDo
|
|||||||
func (r *Repository) FindRefreshTokenByHash(ctx context.Context, hash string) (*RefreshTokenDoc, error) {
|
func (r *Repository) FindRefreshTokenByHash(ctx context.Context, hash string) (*RefreshTokenDoc, error) {
|
||||||
var doc RefreshTokenDoc
|
var doc RefreshTokenDoc
|
||||||
err := r.pool.QueryRow(ctx,
|
err := r.pool.QueryRow(ctx,
|
||||||
`SELECT id, user_id, token_hash, expires_at, created_at FROM refresh_tokens WHERE token_hash = $1`, hash,
|
`SELECT id, user_id, token_hash, expires_at, created_at FROM refresh_tokens WHERE token_hash = $1 AND expires_at > NOW()`, hash,
|
||||||
).Scan(&doc.ID, &doc.UserID, &doc.TokenHash, &doc.ExpiresAt, &doc.CreatedAt)
|
).Scan(&doc.ID, &doc.UserID, &doc.TokenHash, &doc.ExpiresAt, &doc.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -99,6 +75,21 @@ func (r *Repository) DeleteRefreshToken(ctx context.Context, id string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) UpdateUserUsername(ctx context.Context, id, username string) error {
|
||||||
|
_, err := r.pool.Exec(ctx, `UPDATE users SET username = $1 WHERE id = $2`, username, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) UpdateUserPassword(ctx context.Context, id, passwordHash string) error {
|
||||||
|
_, err := r.pool.Exec(ctx, `UPDATE users SET password_hash = $1 WHERE id = $2`, passwordHash, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) DeleteExpiredRefreshTokens(ctx context.Context) error {
|
||||||
|
_, err := r.pool.Exec(ctx, `DELETE FROM refresh_tokens WHERE expires_at <= NOW()`)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) DeleteRefreshTokenByHash(ctx context.Context, hash string) (bool, error) {
|
func (r *Repository) DeleteRefreshTokenByHash(ctx context.Context, hash string) (bool, error) {
|
||||||
tag, err := r.pool.Exec(ctx, `DELETE FROM refresh_tokens WHERE token_hash = $1`, hash)
|
tag, err := r.pool.Exec(ctx, `DELETE FROM refresh_tokens WHERE token_hash = $1`, hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -107,8 +98,4 @@ func (r *Repository) DeleteRefreshTokenByHash(ctx context.Context, hash string)
|
|||||||
return tag.RowsAffected() > 0, nil
|
return tag.RowsAffected() > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) EnsureIndexes(ctx context.Context) error {
|
|
||||||
return r.Migrate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrNoRows = pgx.ErrNoRows
|
var ErrNoRows = pgx.ErrNoRows
|
||||||
|
|||||||
+105
-18
@@ -7,21 +7,24 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrEmailExists = errors.New("email already registered")
|
ErrEmailExists = errors.New("email already registered")
|
||||||
ErrInvalidCreds = errors.New("invalid email or password")
|
ErrInvalidCreds = errors.New("invalid email or password")
|
||||||
ErrUserNotFound = errors.New("user not found")
|
ErrUserNotFound = errors.New("user not found")
|
||||||
ErrInvalidUserID = errors.New("invalid user ID")
|
ErrInvalidUserID = errors.New("invalid user ID")
|
||||||
ErrInvalidRefresh = errors.New("invalid refresh token")
|
ErrInvalidRefresh = errors.New("invalid refresh token")
|
||||||
ErrRefreshExpired = errors.New("refresh token expired")
|
ErrRefreshExpired = errors.New("refresh token expired")
|
||||||
ErrLogoutInvalid = errors.New("refresh token not found or already used")
|
ErrLogoutInvalid = errors.New("refresh token not found or already used")
|
||||||
|
ErrWrongPassword = errors.New("current password is incorrect")
|
||||||
|
ErrWeakPassword = errors.New("password must be at least 8 characters with uppercase, lowercase, and digit")
|
||||||
|
ErrSamePassword = errors.New("new password must differ from current password")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@@ -53,6 +56,27 @@ func generateRandomToken() (string, error) {
|
|||||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validatePasswordStrength(password string) error {
|
||||||
|
if len(password) < 8 {
|
||||||
|
return ErrWeakPassword
|
||||||
|
}
|
||||||
|
var hasUpper, hasLower, hasDigit bool
|
||||||
|
for _, ch := range password {
|
||||||
|
switch {
|
||||||
|
case unicode.IsUpper(ch):
|
||||||
|
hasUpper = true
|
||||||
|
case unicode.IsLower(ch):
|
||||||
|
hasLower = true
|
||||||
|
case unicode.IsDigit(ch):
|
||||||
|
hasDigit = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasUpper || !hasLower || !hasDigit {
|
||||||
|
return ErrWeakPassword
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) issueTokenPair(ctx context.Context, user *User) (*AuthResponse, error) {
|
func (s *Service) issueTokenPair(ctx context.Context, user *User) (*AuthResponse, error) {
|
||||||
accessToken, err := GenerateToken(user.ID, user.Email, s.jwtSecret, s.jwtExp)
|
accessToken, err := GenerateToken(user.ID, user.Email, s.jwtSecret, s.jwtExp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -81,8 +105,13 @@ func (s *Service) issueTokenPair(ctx context.Context, user *User) (*AuthResponse
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Register(ctx context.Context, req RegisterRequest) (*UserPublic, error) {
|
func (s *Service) Register(ctx context.Context, req RegisterRequest) (*AuthResponse, error) {
|
||||||
|
if err := validatePasswordStrength(req.Password); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
req.Email = strings.ToLower(req.Email)
|
req.Email = strings.ToLower(req.Email)
|
||||||
|
|
||||||
existing, err := s.repo.FindByEmail(ctx, req.Email)
|
existing, err := s.repo.FindByEmail(ctx, req.Email)
|
||||||
if err != nil && !errors.Is(err, ErrNoRows) {
|
if err != nil && !errors.Is(err, ErrNoRows) {
|
||||||
return nil, fmt.Errorf("failed to check existing user: %w", err)
|
return nil, fmt.Errorf("failed to check existing user: %w", err)
|
||||||
@@ -103,11 +132,13 @@ func (s *Service) Register(ctx context.Context, req RegisterRequest) (*UserPubli
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.repo.CreateUser(ctx, user); err != nil {
|
if err := s.repo.CreateUser(ctx, user); err != nil {
|
||||||
|
if isPGUniqueViolation(err) {
|
||||||
|
return nil, ErrEmailExists
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
public := NewUserPublic(user)
|
return s.issueTokenPair(ctx, user)
|
||||||
return &public, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Login(ctx context.Context, req LoginRequest) (*AuthResponse, error) {
|
func (s *Service) Login(ctx context.Context, req LoginRequest) (*AuthResponse, error) {
|
||||||
@@ -138,13 +169,6 @@ func (s *Service) Refresh(ctx context.Context, rawRefresh string) (*AuthResponse
|
|||||||
return nil, fmt.Errorf("failed to find refresh token: %w", err)
|
return nil, fmt.Errorf("failed to find refresh token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Now().UTC().After(doc.ExpiresAt) {
|
|
||||||
if err := s.repo.DeleteRefreshToken(ctx, doc.ID); err != nil {
|
|
||||||
log.Printf("failed to cleanup expired refresh token: %v", err)
|
|
||||||
}
|
|
||||||
return nil, ErrRefreshExpired
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.repo.DeleteRefreshToken(ctx, doc.ID); err != nil {
|
if err := s.repo.DeleteRefreshToken(ctx, doc.ID); err != nil {
|
||||||
return nil, fmt.Errorf("failed to delete old refresh token: %w", err)
|
return nil, fmt.Errorf("failed to delete old refresh token: %w", err)
|
||||||
}
|
}
|
||||||
@@ -187,3 +211,66 @@ func (s *Service) GetUserByID(ctx context.Context, userID string) (*UserPublic,
|
|||||||
public := NewUserPublic(user)
|
public := NewUserPublic(user)
|
||||||
return &public, nil
|
return &public, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) ChangePassword(ctx context.Context, userID string, req PasswordChangeRequest) error {
|
||||||
|
if userID == "" {
|
||||||
|
return ErrInvalidUserID
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.repo.FindByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrNoRows) {
|
||||||
|
return ErrUserNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to find user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.OldPassword)); err != nil {
|
||||||
|
return ErrWrongPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.OldPassword == req.NewPassword {
|
||||||
|
return ErrSamePassword
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validatePasswordStrength(req.NewPassword); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to hash password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.UpdateUserPassword(ctx, userID, string(hash)); err != nil {
|
||||||
|
return fmt.Errorf("failed to update password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateProfile(ctx context.Context, userID string, req UpdateProfileRequest) (*UserPublic, error) {
|
||||||
|
if userID == "" {
|
||||||
|
return nil, ErrInvalidUserID
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.repo.FindByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrNoRows) {
|
||||||
|
return nil, ErrUserNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to find user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.UpdateUserUsername(ctx, userID, req.Username); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update username: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Username = req.Username
|
||||||
|
public := NewUserPublic(user)
|
||||||
|
return &public, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPGUniqueViolation(err error) bool {
|
||||||
|
return err != nil && (strings.Contains(err.Error(), "unique") || strings.Contains(err.Error(), "23505"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ func Load() (*Config, error) {
|
|||||||
if cfg.JWTSecret == "" {
|
if cfg.JWTSecret == "" {
|
||||||
return nil, fmt.Errorf("JWT_SECRET is required in .env file")
|
return nil, fmt.Errorf("JWT_SECRET is required in .env file")
|
||||||
}
|
}
|
||||||
|
if len(cfg.JWTSecret) < 32 {
|
||||||
|
return nil, fmt.Errorf("JWT_SECRET must be at least 32 characters long")
|
||||||
|
}
|
||||||
|
|
||||||
if expStr := os.Getenv("JWT_EXPIRATION"); expStr != "" {
|
if expStr := os.Getenv("JWT_EXPIRATION"); expStr != "" {
|
||||||
d, err := time.ParseDuration(expStr)
|
d, err := time.ParseDuration(expStr)
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package org
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
service *Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(service *Service) *Handler {
|
||||||
|
return &Handler{service: service}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Create organization
|
||||||
|
// @Description Create a new organization
|
||||||
|
// @Tags organizations
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param request body CreateOrgRequest true "Organization details"
|
||||||
|
// @Success 201 {object} OrgResponse
|
||||||
|
// @Failure 400 {object} ErrorResponse
|
||||||
|
// @Failure 409 {object} ErrorResponse
|
||||||
|
// @Router /api/organizations [post]
|
||||||
|
func (h *Handler) Create(c *gin.Context) {
|
||||||
|
var req CreateOrgRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
org, err := h.service.Create(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrSlugExists) {
|
||||||
|
c.JSON(http.StatusConflict, ErrorResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("create org error: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, OrgResponse{Organization: *org})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get organization by ID
|
||||||
|
// @Description Get organization details
|
||||||
|
// @Tags organizations
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path string true "Organization ID"
|
||||||
|
// @Success 200 {object} OrgResponse
|
||||||
|
// @Failure 404 {object} ErrorResponse
|
||||||
|
// @Router /api/organizations/{id} [get]
|
||||||
|
func (h *Handler) GetByID(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
org, err := h.service.GetByID(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrNotFound) {
|
||||||
|
c.JSON(http.StatusNotFound, ErrorResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("get org error: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, OrgResponse{Organization: *org})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary List organizations
|
||||||
|
// @Description Get all organizations
|
||||||
|
// @Tags organizations
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Success 200 {object} OrgListResponse
|
||||||
|
// @Failure 500 {object} ErrorResponse
|
||||||
|
// @Router /api/organizations [get]
|
||||||
|
func (h *Handler) List(c *gin.Context) {
|
||||||
|
resp, err := h.service.List(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("list orgs error: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Update organization
|
||||||
|
// @Description Update organization name
|
||||||
|
// @Tags organizations
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path string true "Organization ID"
|
||||||
|
// @Param request body UpdateOrgRequest true "New organization details"
|
||||||
|
// @Success 200 {object} OrgResponse
|
||||||
|
// @Failure 400 {object} ErrorResponse
|
||||||
|
// @Failure 404 {object} ErrorResponse
|
||||||
|
// @Router /api/organizations/{id} [put]
|
||||||
|
func (h *Handler) Update(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
var req UpdateOrgRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
org, err := h.service.Update(c.Request.Context(), id, req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrNotFound) {
|
||||||
|
c.JSON(http.StatusNotFound, ErrorResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("update org error: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, OrgResponse{Organization: *org})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Delete organization
|
||||||
|
// @Description Delete an organization
|
||||||
|
// @Tags organizations
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path string true "Organization ID"
|
||||||
|
// @Success 200 {object} map[string]string
|
||||||
|
// @Failure 404 {object} ErrorResponse
|
||||||
|
// @Router /api/organizations/{id} [delete]
|
||||||
|
func (h *Handler) Delete(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
if err := h.service.Delete(c.Request.Context(), id); err != nil {
|
||||||
|
if errors.Is(err, ErrNotFound) {
|
||||||
|
c.JSON(http.StatusNotFound, ErrorResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("delete org error: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, ErrorResponse{Error: "internal server error"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "organization deleted"})
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package org
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Organization struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateOrgRequest struct {
|
||||||
|
Name string `json:"name" binding:"required,min=2,max=100" example:"My Corp"`
|
||||||
|
Slug string `json:"slug" binding:"required,min=2,max=50" example:"my-corp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateOrgRequest struct {
|
||||||
|
Name string `json:"name" binding:"required,min=2,max=100" example:"My Corp Updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrgResponse struct {
|
||||||
|
Organization Organization `json:"organization"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrgListResponse struct {
|
||||||
|
Organizations []Organization `json:"organizations"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package org
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNoRows = pgx.ErrNoRows
|
||||||
|
|
||||||
|
type Repository struct {
|
||||||
|
pool *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(pool *pgxpool.Pool) *Repository {
|
||||||
|
return &Repository{pool: pool}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) Create(ctx context.Context, org *Organization) error {
|
||||||
|
org.ID = uuid.New().String()
|
||||||
|
now := time.Now().UTC()
|
||||||
|
org.CreatedAt = now
|
||||||
|
org.UpdatedAt = now
|
||||||
|
_, err := r.pool.Exec(ctx,
|
||||||
|
`INSERT INTO organizations (id, name, slug, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)`,
|
||||||
|
org.ID, org.Name, org.Slug, org.CreatedAt, org.UpdatedAt,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) FindByID(ctx context.Context, id string) (*Organization, error) {
|
||||||
|
var org Organization
|
||||||
|
err := r.pool.QueryRow(ctx,
|
||||||
|
`SELECT id, name, slug, created_at, updated_at FROM organizations WHERE id = $1`, id,
|
||||||
|
).Scan(&org.ID, &org.Name, &org.Slug, &org.CreatedAt, &org.UpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &org, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) FindAll(ctx context.Context) ([]Organization, error) {
|
||||||
|
rows, err := r.pool.Query(ctx,
|
||||||
|
`SELECT id, name, slug, created_at, updated_at FROM organizations ORDER BY created_at DESC`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var orgs []Organization
|
||||||
|
for rows.Next() {
|
||||||
|
var org Organization
|
||||||
|
if err := rows.Scan(&org.ID, &org.Name, &org.Slug, &org.CreatedAt, &org.UpdatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
orgs = append(orgs, org)
|
||||||
|
}
|
||||||
|
return orgs, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) Update(ctx context.Context, org *Organization) error {
|
||||||
|
org.UpdatedAt = time.Now().UTC()
|
||||||
|
_, err := r.pool.Exec(ctx,
|
||||||
|
`UPDATE organizations SET name = $1, updated_at = $2 WHERE id = $3`,
|
||||||
|
org.Name, org.UpdatedAt, org.ID,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) Delete(ctx context.Context, id string) error {
|
||||||
|
_, err := r.pool.Exec(ctx, `DELETE FROM organizations WHERE id = $1`, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package org
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("organization not found")
|
||||||
|
ErrSlugExists = errors.New("slug already taken")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
repo *Repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo *Repository) *Service {
|
||||||
|
return &Service{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Create(ctx context.Context, req CreateOrgRequest) (*Organization, error) {
|
||||||
|
req.Slug = strings.ToLower(strings.TrimSpace(req.Slug))
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
Name: req.Name,
|
||||||
|
Slug: req.Slug,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.Create(ctx, org); err != nil {
|
||||||
|
if isUniqueViolation(err) {
|
||||||
|
return nil, ErrSlugExists
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to create organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return org, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetByID(ctx context.Context, id string) (*Organization, error) {
|
||||||
|
org, err := s.repo.FindByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrNoRows) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to find organization: %w", err)
|
||||||
|
}
|
||||||
|
return org, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) List(ctx context.Context) (*OrgListResponse, error) {
|
||||||
|
orgs, err := s.repo.FindAll(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list organizations: %w", err)
|
||||||
|
}
|
||||||
|
if orgs == nil {
|
||||||
|
orgs = []Organization{}
|
||||||
|
}
|
||||||
|
return &OrgListResponse{
|
||||||
|
Organizations: orgs,
|
||||||
|
Total: len(orgs),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Update(ctx context.Context, id string, req UpdateOrgRequest) (*Organization, error) {
|
||||||
|
org, err := s.repo.FindByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrNoRows) {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to find organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
org.Name = req.Name
|
||||||
|
|
||||||
|
if err := s.repo.Update(ctx, org); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return org, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(ctx context.Context, id string) error {
|
||||||
|
org, err := s.repo.FindByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrNoRows) {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to find organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.repo.Delete(ctx, org.ID); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isUniqueViolation(err error) bool {
|
||||||
|
return err != nil && (strings.Contains(err.Error(), "unique") || strings.Contains(err.Error(), "23505"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
token_hash TEXT NOT NULL UNIQUE,
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires_at ON refresh_tokens(expires_at);
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS refresh_tokens;
|
||||||
|
DROP TABLE IF EXISTS users;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE IF NOT EXISTS organizations (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
slug TEXT NOT NULL UNIQUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE IF EXISTS organizations;
|
||||||
Reference in New Issue
Block a user