Skip to content

Commit 3dc84fd

Browse files
committed
feat: implement registration flow
Signed-off-by: krishna2803 <kpandey81930@gmail.com>
1 parent b010cef commit 3dc84fd

File tree

11 files changed

+474
-2
lines changed

11 files changed

+474
-2
lines changed

cmd/nymeria/main.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
// Copyright (c) 2025 SDSLabs
2+
// SPDX-License-Identifier: MIT
3+
14
package main
25

3-
import "fmt"
6+
import "github.com/sdslabs/nymeria/internal/api"
47

58
func main() {
6-
fmt.Printf("Accounts V3")
9+
api.Start()
710
}

internal/api/login.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) 2025 SDSLabs
2+
// SPDX-License-Identifier: MIT
3+
4+
package api
5+
6+
import (
7+
"net/http"
8+
)
9+
10+
// HandleGetLoginFlow handles the GET request for the login flow.
11+
func HandleGetLoginFlow(w http.ResponseWriter, r *http.Request) {
12+
if r.Method != http.MethodGet {
13+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
14+
return
15+
}
16+
17+
w.Write([]byte("not implemented"))
18+
}
19+
20+
// HandlePostLoginFlow handles the POST request for the login flow.
21+
func HandlePostLoginFlow(w http.ResponseWriter, r *http.Request) {
22+
if r.Method != http.MethodPost {
23+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
24+
return
25+
}
26+
27+
w.Write([]byte("not implemented"))
28+
}

internal/api/main.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) 2025 SDSLabs
2+
// SPDX-License-Identifier: MIT
3+
4+
package api
5+
6+
import (
7+
"net/http"
8+
9+
"github.com/go-chi/chi/middleware"
10+
"github.com/go-chi/chi/v5"
11+
"github.com/go-chi/cors"
12+
"github.com/sdslabs/nymeria/internal/database"
13+
)
14+
15+
func Start() {
16+
if err := database.Init(); err != nil {
17+
panic(err)
18+
}
19+
20+
r := chi.NewRouter()
21+
22+
// r.Use(log.LoggerMiddleware(log.Logger))
23+
r.Use(middleware.Logger)
24+
25+
// CORS configuration
26+
r.Use(cors.Handler(cors.Options{
27+
AllowedOrigins: []string{"http://localhost:3000"}, // TODO: change in prod
28+
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
29+
AllowedHeaders: []string{"Authorization", "Content-Type"},
30+
ExposedHeaders: []string{"Content-Length"},
31+
AllowCredentials: true,
32+
MaxAge: 12 * 3600, // 12 hours in seconds
33+
}))
34+
35+
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
36+
w.Write([]byte("welcome"))
37+
})
38+
39+
r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
40+
w.Write([]byte("pong! 🏓"))
41+
})
42+
43+
r.Get("/register", HandleGetRegistrationFlow)
44+
r.Post("/register", HandlePostRegistrationFlow)
45+
46+
r.Get("/login", HandleGetLoginFlow)
47+
r.Post("/login", HandlePostLoginFlow)
48+
49+
http.ListenAndServe(":9898", r)
50+
}

internal/api/recovery.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) 2025 SDSLabs
2+
// SPDX-License-Identifier: MIT
3+
4+
package api
5+
6+
import "net/http"
7+
8+
func HandleGetRecoveryFlow(w http.ResponseWriter, r *http.Request) {
9+
10+
}
11+
12+
func HandlePostRecoveryFlow(w http.ResponseWriter, r *http.Request) {
13+
14+
}

internal/api/register.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) 2025 SDSLabs
2+
// SPDX-License-Identifier: MIT
3+
4+
package api
5+
6+
import (
7+
"encoding/json"
8+
"log"
9+
"net/http"
10+
"strings"
11+
12+
"github.com/sdslabs/nymeria/internal/database"
13+
)
14+
15+
// HandleGetRegistrationFlow handles the GET request for the registration flow.
16+
func HandleGetRegistrationFlow(w http.ResponseWriter, r *http.Request) {
17+
if r.Method != http.MethodGet {
18+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
19+
return
20+
}
21+
22+
w.Write([]byte("not implemented"))
23+
}
24+
25+
// HandlePostRegistrationFlow handles the POST request for the registration flow.
26+
func HandlePostRegistrationFlow(w http.ResponseWriter, r *http.Request) {
27+
if r.Method != http.MethodPost {
28+
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
29+
return
30+
}
31+
32+
var req RegistrationRequest
33+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
34+
http.Error(w, "Invalid JSON", http.StatusBadRequest)
35+
return
36+
}
37+
38+
if req.Username == "" {
39+
http.Error(w, "Username is required", http.StatusBadRequest)
40+
return
41+
}
42+
if req.Password == "" {
43+
http.Error(w, "Password is required", http.StatusBadRequest)
44+
return
45+
}
46+
if req.Email == "" {
47+
http.Error(w, "Email is required", http.StatusBadRequest)
48+
return
49+
}
50+
if req.Phone == "" {
51+
http.Error(w, "Phone number is required", http.StatusBadRequest)
52+
return
53+
}
54+
55+
user := database.User{
56+
Username: req.Username,
57+
Password: req.Password,
58+
Email: req.Email,
59+
Phone: req.Phone,
60+
}
61+
62+
if result := database.DB.Create(&user); result.Error != nil {
63+
if strings.Contains(result.Error.Error(), "duplicate key") {
64+
http.Error(w, "GitHub ID already exists", http.StatusConflict)
65+
} else {
66+
log.Fatalf("failed to insert user: %v", result.Error)
67+
}
68+
}
69+
70+
w.WriteHeader(http.StatusCreated)
71+
72+
response := map[string]string{
73+
"message": "User registered successfully",
74+
}
75+
76+
jsonResponse, err := json.Marshal(response)
77+
if err != nil {
78+
http.Error(w, "Failed to create response: "+err.Error(), http.StatusInternalServerError)
79+
return
80+
}
81+
w.Header().Set("Content-Type", "application/json")
82+
w.Write(jsonResponse)
83+
}

internal/api/types.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package api
2+
3+
type RegistrationRequest struct {
4+
Username string `json:"username"`
5+
Password string `json:"password"`
6+
Email string `json:"email"`
7+
Phone string `json:"phone"`
8+
}
9+
10+
type LoginRequest struct {
11+
Username string `json:"username"`
12+
Password string `json:"password"`
13+
}

internal/database/database.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) 2025 SDSLabs
2+
// SPDX-License-Identifier: MIT
3+
4+
package database
5+
6+
import (
7+
"fmt"
8+
"log"
9+
"os"
10+
11+
"gorm.io/driver/postgres"
12+
"gorm.io/gorm"
13+
"gorm.io/gorm/logger"
14+
)
15+
16+
// The global database instance
17+
var DB *gorm.DB
18+
19+
// loadConfig loads database configuration from environment variables
20+
func loadConfig() *Config {
21+
return &Config{
22+
// TODO: update this
23+
Host: getEnvOrDefault("DB_HOST", "localhost"),
24+
User: getEnvOrDefault("DB_USER", "nymeria"),
25+
Password: getEnvOrDefault("DB_PASS", "password"),
26+
DBName: getEnvOrDefault("DB_NAME", "nymeria"),
27+
Port: getEnvOrDefault("DB_PORT", "5432"),
28+
}
29+
}
30+
31+
// getEnvOrDefault retrieves the value of the environment variable named by key.
32+
func getEnvOrDefault(key, defaultValue string) string {
33+
if value := os.Getenv(key); value != "" {
34+
return value
35+
}
36+
return defaultValue
37+
}
38+
39+
// connect establishes a connection to the PostgreSQL database using GORM
40+
func connect(config *Config) (*gorm.DB, error) {
41+
if config.Password == "" {
42+
return nil, fmt.Errorf("DB_PASS environment variable is required")
43+
}
44+
45+
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
46+
config.Host, config.User, config.Password, config.DBName, config.Port)
47+
48+
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
49+
Logger: logger.Default.LogMode(logger.Silent),
50+
})
51+
if err != nil {
52+
return nil, fmt.Errorf("failed to connect to database: %w", err)
53+
}
54+
55+
if err := db.AutoMigrate(&User{}, &Organization{}, &Application{}); err != nil {
56+
return nil, fmt.Errorf("failed to auto migrate database: %w", err)
57+
}
58+
59+
return db, nil
60+
}
61+
62+
// Init initializes the database connection
63+
func Init() error {
64+
config := loadConfig()
65+
66+
db, err := connect(config)
67+
if err != nil {
68+
return fmt.Errorf("database initialization failed: %w", err)
69+
}
70+
71+
DB = db
72+
log.Println("Database connected successfully")
73+
return nil
74+
}
75+
76+
// Close closes the database connection
77+
func Close() error {
78+
if DB == nil {
79+
return nil
80+
}
81+
82+
sqlDB, err := DB.DB()
83+
if err != nil {
84+
return fmt.Errorf("failed to get underlying sql.DB: %w", err)
85+
}
86+
87+
return sqlDB.Close()
88+
}

internal/database/types.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) 2025 SDSLabs
2+
// SPDX-License-Identifier: MIT
3+
4+
package database
5+
6+
import "gorm.io/gorm"
7+
8+
// Config holds database configuration
9+
type Config struct {
10+
Host string
11+
User string
12+
Password string
13+
DBName string
14+
Port string
15+
}
16+
17+
// User holds user info in the database
18+
type User struct {
19+
gorm.Model
20+
Username string `json:"username" gorm:"not null"`
21+
Password string `json:"password" gorm:"not null"`
22+
// Phone string `json:"phone" gorm:"unique; not null; type:char(10)"`
23+
// Email string `json:"email" gorm:"unique; not null; type:varchar(255)"`
24+
Phone string `json:"phone" gorm:"not null; type:char(10)"`
25+
Email string `json:"email" gorm:"not null; type:varchar(255)"`
26+
Role string `json:"role" gorm:"default:user; not null"`
27+
ImageURL string `json:"image_url" gorm:"default:''; not null; type:varchar(255)"`
28+
// GitHubID string `json:"github_id" gorm:"unique; not null; type:varchar(255)"`
29+
GitHubID string `json:"github_id" gorm:"not null; type:varchar(255)"`
30+
AccountStatus string `json:"account_status" gorm:"default:active; not null"` // active, banned, account takeover, etc.
31+
Verified bool `json:"verified" gorm:"default:false; not null"`
32+
AdditionalEmails string `json:"additional_emails" gorm:"type:text; default:'{}'"`
33+
34+
// TODO: add apps after GBM discussion
35+
}
36+
37+
type Organization struct {
38+
gorm.Model
39+
Name string `json:"name" gorm:"not null; type:varchar(255)"`
40+
}
41+
42+
type Application struct {
43+
gorm.Model
44+
Name string `json:"name" gorm:"not null; type:varchar(255)"`
45+
Description string `json:"description" gorm:"not null; type:varchar(255)"`
46+
AppURL string `json:"app_url" gorm:"not null; type:varchar(255)"`
47+
AllowedDomains string `json:"allowed_domains" gorm:"not null; type:varchar(255)"`
48+
OrganizationID uint `json:"organization_id" gorm:"not null"`
49+
Organization Organization `json:"organization" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:OrganizationID"`
50+
}

0 commit comments

Comments
 (0)