Skip to content

Commit cd0bd2c

Browse files
committed
Search User Implemented also test added
1 parent b2b9f53 commit cd0bd2c

7 files changed

Lines changed: 141 additions & 21 deletions

File tree

server/controllers/users.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,44 @@
11
package controllers
22

3-
import "net/http"
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"strconv"
7+
8+
"github.com/commandlinecoding/elephant/server/models"
9+
"github.com/commandlinecoding/elephant/server/services"
10+
)
411

512
func HandleUsers(w http.ResponseWriter, r *http.Request) {
613
w.Header().Set("Content-Type", "application/json")
714
_, _ = w.Write([]byte(`{"status": "users stub active"}`))
8-
}
15+
}
16+
17+
func HandleUserSearch(w http.ResponseWriter, r *http.Request) {
18+
w.Header().Set("Content-Type", "application/json")
19+
20+
// Extract parameters
21+
q := r.URL.Query().Get("q")
22+
pageStr := r.URL.Query().Get("page")
23+
limitStr := r.URL.Query().Get("limit")
24+
25+
page, _ := strconv.Atoi(pageStr)
26+
limit, _ := strconv.Atoi(limitStr)
27+
28+
svc := services.NewUserService()
29+
users, err := svc.Search(r.Context(), q, page, limit)
30+
if err != nil {
31+
w.WriteHeader(http.StatusInternalServerError)
32+
_ = json.NewEncoder(w).Encode(models.JSONResponse{
33+
Success: false,
34+
Error: "Failed to execute user directory search operations",
35+
})
36+
return
37+
}
38+
39+
w.WriteHeader(http.StatusOK)
40+
_ = json.NewEncoder(w).Encode(models.JSONResponse{
41+
Success: true,
42+
Data: users,
43+
})
44+
}

server/env/env.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,45 @@
11
package env
22

33
import (
4-
"log"
54
"os"
5+
"path/filepath"
66

77
"github.com/joho/godotenv"
88
)
99

1010
// global variables
1111
var (
12-
// Server configurations
1312
HOST string
1413
PORT string
1514

16-
// PostgreSQL database coordinates
1715
POSTGRES_USER string
1816
POSTGRES_PASSWORD string
1917
POSTGRES_DB string
2018
POSTGRES_PORT string
2119
POSTGRES_HOST string
2220

23-
// Security & Token configurations
2421
JWT_SECRET string
2522

26-
// Cryptography parameters (Argon2id tuning)
2723
ARGON_MEMORY string
2824
ARGON_ITERATIONS string
2925
ARGON_PARALLELISM string
3026
)
3127

3228
func init() {
33-
// load the file into the environment
34-
if _, err := os.Stat("../.env"); err == nil {
35-
if err := godotenv.Load("../.env"); err != nil {
36-
log.Printf("Warning: Found .env but failed to parse it: %v", err)
29+
cwd, err := os.Getwd()
30+
if err == nil {
31+
for {
32+
target := filepath.Join(cwd, ".env")
33+
if _, err := os.Stat(target); err == nil {
34+
_ = godotenv.Load(target)
35+
break
36+
}
37+
parent := filepath.Dir(cwd)
38+
if parent == cwd {
39+
break
40+
}
41+
cwd = parent
3742
}
38-
} else if _, err := os.Stat(".env"); err == nil {
39-
_ = godotenv.Load(".env")
4043
}
4144

4245
// assign the values with fallbacks
@@ -56,10 +59,9 @@ func init() {
5659
ARGON_PARALLELISM = getEnv("ARGON_PARALLELISM", "2")
5760
}
5861

59-
// Smart fallback defaults if an env variable is missing locally
6062
func getEnv(key, fallback string) string {
6163
if value, exists := os.LookupEnv(key); exists {
6264
return value
6365
}
6466
return fallback
65-
}
67+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
2+
CREATE INDEX IF NOT EXISTS idx_users_display_name ON users(display_name);

server/repository/user.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,38 @@ func (r *UserRepository) FindByUsername(ctx context.Context, username string) (*
6868
return nil, err
6969
}
7070
return &user, nil
71-
}
71+
}
72+
73+
func (r *UserRepository) SearchUsers(ctx context.Context, searchTerm string, limit, offset int) ([]models.User, error) {
74+
pattern := "%" + searchTerm + "%"
75+
76+
query := `
77+
SELECT id, username, display_name, created_at
78+
FROM users
79+
WHERE username ILIKE $1 OR display_name ILIKE $2
80+
ORDER BY username ASC
81+
LIMIT $3 OFFSET $4;
82+
`
83+
84+
rows, err := config.DB.Query(ctx, query, pattern, pattern, limit, offset)
85+
if err != nil {
86+
return nil, err
87+
}
88+
defer rows.Close()
89+
90+
var users []models.User = []models.User{}
91+
for rows.Next() {
92+
var u models.User
93+
err := rows.Scan(&u.ID, &u.Username, &u.DisplayName, &u.CreatedAt)
94+
if err != nil {
95+
return nil, err
96+
}
97+
users = append(users, u)
98+
}
99+
100+
if err = rows.Err(); err != nil {
101+
return nil, err
102+
}
103+
104+
return users, nil
105+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package repository
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
"github.com/commandlinecoding/elephant/server/config"
9+
)
10+
11+
func TestMain(m *testing.M) {
12+
config.InitDatabase()
13+
code := m.Run()
14+
if config.DB != nil {
15+
config.DB.Close()
16+
}
17+
os.Exit(code)
18+
}
19+
20+
func BenchmarkSearchUsers(b *testing.B) {
21+
repo := NewUserRepository()
22+
ctx := context.Background()
23+
24+
b.ResetTimer()
25+
for i := 0; i < b.N; i++ {
26+
_, err := repo.SearchUsers(ctx, "testuser", 20, 0)
27+
if err != nil {
28+
b.Fatalf("Benchmark error: %v", err)
29+
}
30+
}
31+
}

server/routes/users.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ import (
77

88
func UserRoute(router chi.Router) {
99
router.Get("/", controllers.HandleUsers) // resolves to /api/users
10+
router.Get("/search", controllers.HandleUserSearch) // resolves to /api/users/search
1011
}

server/services/users.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ func NewUserService() *UserService {
2424
func (s *UserService) Register(ctx context.Context, requestedName, dn, passwd string) (*models.User, error) {
2525
requestedName = strings.ToLower(strings.TrimSpace(requestedName))
2626
dn = strings.TrimSpace(dn)
27-
27+
2828
if len(requestedName) < 3 || len(requestedName) > 25 {
2929
return nil, errors.New("username prefix must be between 3 and 25 characters")
3030
}
31-
31+
3232
isAlphanumeric := regexp.MustCompile(`^[a-z0-9]+$`).MatchString
3333
if !isAlphanumeric(requestedName) {
3434
return nil, errors.New("username prefix must contain only alphanumeric characters")
@@ -52,9 +52,9 @@ func (s *UserService) Register(ctx context.Context, requestedName, dn, passwd st
5252
return nil, errors.New("failed to generate secure user discriminator")
5353
}
5454
discriminator := nBig.Int64() + 1000 // forces range [1000, 9999]
55-
55+
5656
candidate := fmt.Sprintf("%s.%d", requestedName, discriminator)
57-
57+
5858
exists, err := s.repo.UsernameExists(ctx, candidate)
5959
if err != nil {
6060
return nil, err
@@ -76,4 +76,18 @@ func (s *UserService) Register(ctx context.Context, requestedName, dn, passwd st
7676
}
7777

7878
return s.repo.CreateUser(ctx, finalUsername, dn, hash)
79-
}
79+
}
80+
81+
func (s *UserService) Search(ctx context.Context, query string, page, limit int) ([]models.User, error) {
82+
query = strings.TrimSpace(query)
83+
84+
if limit <= 0 || limit > 100 {
85+
limit = 20
86+
}
87+
if page <= 0 {
88+
page = 1
89+
}
90+
offset := (page - 1) * limit
91+
92+
return s.repo.SearchUsers(ctx, query, limit, offset)
93+
}

0 commit comments

Comments
 (0)