Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions backend/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ paths:
required:
- access_token
- user
- needs_mfa
properties:
access_token:
type: string
Expand All @@ -104,6 +105,10 @@ paths:
format: uuid
description: The unique identifier of the newly created therapist
example: "f20e5948-01ba-4113-b453-db05d8bde3bc"
needs_mfa:
type: boolean
description: Indicates if the user needs multi-factor authentication
example: false
"400":
description: Bad Request (e.g., validation errors)
content:
Expand Down Expand Up @@ -165,6 +170,7 @@ paths:
- expires_in
- refresh_token
- user
- needs_mfa
properties:
access_token:
type: string
Expand All @@ -190,6 +196,10 @@ paths:
format: uuid
description: The unique identifier of the newly created therapist
example: "f20e5948-01ba-4113-b453-db05d8bde3bc"
needs_mfa:
type: boolean
description: Indicates if the user needs multi-factor authentication
example: false
error:
content:
application/json:
Expand Down
11 changes: 0 additions & 11 deletions backend/env.sample

This file was deleted.

24 changes: 24 additions & 0 deletions backend/env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
DB_USER=
DB_PASSWORD=
DB_HOST=
DB_PORT=
DB_NAME=

PORT=

SUPABASE_URL=
SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
AWS_S3_BUCKET=

RESEND_API_KEY=
EMAIL_VERIFICATION_ENABLED=true
RESEND_FROM_EMAIL=

DB_MAX_OPEN_CONNS=2
DB_MAX_IDLE_CONNS=0
DB_CONN_MAX_LIFETIME=300
74 changes: 32 additions & 42 deletions backend/internal/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,26 @@ import (
"specialstandard/internal/errs"

"github.com/goccy/go-json"
"github.com/google/uuid"
)

type userResponse struct {
ID uuid.UUID `json:"id"`
}

type SignInResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
User userResponse `json:"user"`
Error interface{} `json:"error"`
}
"specialstandard/internal/models"
)

func SupabaseLogin(cfg *config.Supabase, email string, password string) (SignInResponse, error) {
func SupabaseLogin(cfg *config.Supabase, email string, password string, needsEmailVerification bool) (models.SignInResponse, error) {
supabaseURL := cfg.URL
serviceroleKey := cfg.ServiceRoleKey

payload := Payload{
payload := models.Payload{
Email: email,
Password: password,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return SignInResponse{}, err
return models.SignInResponse{}, err
}

req, err := http.NewRequest("POST", fmt.Sprintf("%s/auth/v1/token?grant_type=password", supabaseURL), bytes.NewBuffer(payloadBytes))
if err != nil {
return SignInResponse{}, err
return models.SignInResponse{}, err
}

req.Header.Set("Content-Type", "application/json")
Expand All @@ -51,7 +39,7 @@ func SupabaseLogin(cfg *config.Supabase, email string, password string) (SignInR
res, err := Client.Do(req)
if err != nil {
slog.Error("Failed to execute Request", "err", err)
return SignInResponse{}, errs.BadRequest("Failed to execute Request")
return models.SignInResponse{}, errs.BadRequest("Failed to execute Request")
}
defer func() {
_ = res.Body.Close()
Expand All @@ -60,40 +48,42 @@ func SupabaseLogin(cfg *config.Supabase, email string, password string) (SignInR
body, err := io.ReadAll(res.Body)
if err != nil {
slog.Error("Failed to read response body", "err", err)
return SignInResponse{}, errs.BadRequest("Failed to read response body")
return models.SignInResponse{}, errs.BadRequest("Failed to read response body")
}

if res.StatusCode != http.StatusOK {
// Try to parse the error response
var errorResp struct {
Message string `json:"msg"`
Error string `json:"error"`
}

if err := json.Unmarshal(body, &errorResp); err == nil {
// Use the parsed message if available
if errorResp.Message != "" {
return SignInResponse{}, errs.BadRequest(errorResp.Message)
}
if errorResp.Error != "" {
return SignInResponse{}, errs.BadRequest(errorResp.Error)
}
}

// Fallback to generic message if parsing fails
return SignInResponse{}, errs.BadRequest("Invalid credentials")
}
// Try to parse the error response
var errorResp struct {
Message string `json:"msg"`
Error string `json:"error"`
}

if err := json.Unmarshal(body, &errorResp); err == nil {
// Use the parsed message if available
if errorResp.Message != "" {
return models.SignInResponse{}, errs.BadRequest(errorResp.Message)
}
if errorResp.Error != "" {
return models.SignInResponse{}, errs.BadRequest(errorResp.Error)
}
}

var signInResponse SignInResponse
// Fallback to generic message if parsing fails
return models.SignInResponse{}, errs.BadRequest("Invalid credentials")
}

var signInResponse models.SignInResponse
err = json.Unmarshal(body, &signInResponse)
if err != nil {
slog.Error("Failed to parse response body", "body", err)
return SignInResponse{}, errs.BadRequest("Failed to parse response body")
return models.SignInResponse{}, errs.BadRequest("Failed to parse response body")
}

if signInResponse.Error != nil {
return SignInResponse{}, errs.BadRequest(fmt.Sprintf("Sign In Response Error %v", signInResponse.Error))
return models.SignInResponse{}, errs.BadRequest(fmt.Sprintf("Sign In Response Error %v", signInResponse.Error))
}

signInResponse.RequiresMFA = needsEmailVerification

return signInResponse, nil
}
39 changes: 14 additions & 25 deletions backend/internal/auth/signup.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,9 @@ import (
"specialstandard/internal/errs"

"github.com/goccy/go-json"
"github.com/google/uuid"
)

type Payload struct {
Email string `json:"email"`
Password string `json:"password"`
}

type UserSignupResponse struct {
ID uuid.UUID `json:"id"`
}

type SignupResponse struct {
AccessToken string `json:"access_token"`
User UserSignupResponse `json:"user"`
}
"specialstandard/internal/models"
)

func validatePasswordStrength(password string) error {
if len(password) < 8 {
Expand All @@ -45,27 +32,27 @@ func validatePasswordStrength(password string) error {
return nil
}

func SupabaseSignup(cfg *config.Supabase, email, password string) (SignupResponse, error) {
func SupabaseSignup(cfg *config.Supabase, email, password string, needsEmailVerification bool) (models.SignupResponse, error) {
if err := validatePasswordStrength(password); err != nil {
return SignupResponse{}, errs.BadRequest(fmt.Sprintf("Weak Password: %v", err))
return models.SignupResponse{}, errs.BadRequest(fmt.Sprintf("Weak Password: %v", err))
}

supabaseURL := cfg.URL
apiKey := cfg.ServiceRoleKey

payload := Payload{
payload := models.Payload{
Email: email,
Password: password,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return SignupResponse{}, err
return models.SignupResponse{}, err
}

req, err := http.NewRequest("POST", fmt.Sprintf("%s/auth/v1/signup", supabaseURL), bytes.NewBuffer(payloadBytes))
if err != nil {
slog.Error("Error in Request Creation: ", "err", err)
return SignupResponse{}, errs.BadRequest(fmt.Sprintf("Failed to create request: %v", err))
return models.SignupResponse{}, errs.BadRequest(fmt.Sprintf("Failed to create request: %v", err))
}

req.Header.Set("Content-Type", "application/json")
Expand All @@ -75,7 +62,7 @@ func SupabaseSignup(cfg *config.Supabase, email, password string) (SignupRespons
res, err := Client.Do(req)
if err != nil {
slog.Error("Error executing request: ", "err", err)
return SignupResponse{}, errs.BadRequest(fmt.Sprintf("Failed to execute request: %v, %s", err, supabaseURL))
return models.SignupResponse{}, errs.BadRequest(fmt.Sprintf("Failed to execute request: %v, %s", err, supabaseURL))
}
defer func() {
_ = res.Body.Close()
Expand All @@ -84,19 +71,21 @@ func SupabaseSignup(cfg *config.Supabase, email, password string) (SignupRespons
body, err := io.ReadAll(res.Body)
if err != nil {
slog.Error("Error reading response body: ", "body", body)
return SignupResponse{}, errs.BadRequest("Failed to read response body", string(body))
return models.SignupResponse{}, errs.BadRequest("Failed to read response body", string(body))
}

if res.StatusCode != http.StatusOK {
slog.Error("Error Response: ", "res.StatusCode", res.StatusCode, "body", string(body))
return SignupResponse{}, errs.BadRequest(fmt.Sprintf("Failed to login %d, %s", res.StatusCode, body))
return models.SignupResponse{}, errs.BadRequest(fmt.Sprintf("Failed to login %d, %s", res.StatusCode, body))
}

var response SignupResponse
var response models.SignupResponse
if err := json.Unmarshal(body, &response); err != nil {
slog.Error("Error parsing response: ", "err", err)
return SignupResponse{}, errs.BadRequest("Failed to parse request")
return models.SignupResponse{}, errs.BadRequest("Failed to parse request")
}

response.RequiresMFA = needsEmailVerification

return response, nil
}
34 changes: 34 additions & 0 deletions backend/internal/models/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package models

import (
"github.com/google/uuid"
)

type userResponse struct {
ID uuid.UUID `json:"id"`
}

type SignInResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
User userResponse `json:"user"`
Error interface{} `json:"error"`
RequiresMFA bool `json:"needs_mfa"`
}

type Payload struct {
Email string `json:"email"`
Password string `json:"password"`
}

type UserSignupResponse struct {
ID uuid.UUID `json:"id"`
}

type SignupResponse struct {
AccessToken string `json:"access_token"`
User UserSignupResponse `json:"user"`
RequiresMFA bool `json:"needs_mfa"`
}
8 changes: 5 additions & 3 deletions backend/internal/service/handler/auth/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import (
)

type Handler struct {
config config.Supabase
therapistRepository storage.TherapistRepository
config config.Supabase
therapistRepository storage.TherapistRepository
emailVerificationEnabled bool
}

type Credentials struct {
Expand All @@ -18,9 +19,10 @@ type Credentials struct {
RememberMe bool `json:"remember_me"`
}

func NewHandler(config config.Supabase, therapistRepository storage.TherapistRepository) *Handler {
func NewHandler(config config.Supabase, therapistRepository storage.TherapistRepository, emailVerificationEnabled bool) *Handler {
return &Handler{
config,
therapistRepository,
emailVerificationEnabled,
}
}
4 changes: 2 additions & 2 deletions backend/internal/service/handler/auth/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func TestHandler_SignUp(t *testing.T) {
ServiceRoleKey: "SRK",
}

handler := NewHandler(mockConfig, mockRepo)
handler := NewHandler(mockConfig, mockRepo, true)
app.Post("/signup", handler.SignUp)

req := httptest.NewRequest("POST", "/signup", strings.NewReader(tt.payload))
Expand Down Expand Up @@ -152,7 +152,7 @@ func TestHandler_Login(t *testing.T) {
ServiceRoleKey: "SRK",
}

handler := NewHandler(mockConfig, mockRepo)
handler := NewHandler(mockConfig, mockRepo, true)
app.Post("/login", handler.Login)

req := httptest.NewRequest("POST", "/login", strings.NewReader(tt.payload))
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/service/handler/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func (h *Handler) Login(c *fiber.Ctx) error {
return errs.BadRequest(fmt.Sprintf("Invalid Request Body: %v", cred))
}

signInResponse, err := auth.SupabaseLogin(&h.config, cred.Email, cred.Password)
signInResponse, err := auth.SupabaseLogin(&h.config, cred.Email, cred.Password, h.emailVerificationEnabled)
if err != nil {
slog.Error("Supabase Login Error: ", "err", err.Error())

Expand Down
2 changes: 1 addition & 1 deletion backend/internal/service/handler/auth/signup.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (h *Handler) SignUp(c *fiber.Ctx) error {
return errs.BadRequest(fmt.Sprintf("Invalid Request Body: %v", err))
}

res, err := auth.SupabaseSignup(&h.config, cred.Email, cred.Password)
res, err := auth.SupabaseSignup(&h.config, cred.Email, cred.Password, h.emailVerificationEnabled)
if err != nil {
slog.Error(fmt.Sprintf("Signup Request Failed: %v", err))
return errs.InternalServerError(fmt.Sprintf("Signup Request Failed: %v", err))
Expand Down
Loading
Loading