Skip to content

Commit aebd75a

Browse files
committed
Update to API spec
1 parent b3d8b2a commit aebd75a

File tree

4 files changed

+133
-0
lines changed

4 files changed

+133
-0
lines changed

src/auth/otp/otp.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package otp
2+
3+
import (
4+
"fmt"
5+
"math/rand"
6+
"time"
7+
)
8+
9+
const OTP_EXPIRATION_TIME = 15 * time.Minute
10+
11+
func GenerateOTP() (string, time.Time, error) {
12+
// Generate a random OTP (6 digits)
13+
otp := fmt.Sprintf("%06d", rand.Intn(1000000))
14+
exp_date := time.Now().Add(OTP_EXPIRATION_TIME)
15+
return otp, exp_date, nil
16+
}
17+
18+
func ValidateOTP(otp string, expDate time.Time) (bool, error) {
19+
if time.Now().After(expDate) {
20+
return false, fmt.Errorf("OTP has expired")
21+
}
22+
return true, nil
23+
}

src/dao/user.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@ package dao
22

33
import (
44
"OPP/auth/api"
5+
"OPP/auth/auth/otp"
56
"OPP/auth/db"
67
"context"
78
"errors"
89
"fmt"
910
"strings"
11+
"time"
1012
)
1113

1214
var (
1315
ErrUserAlreadyExists = errors.New("user already exists")
1416
ErrInvalidUser = errors.New("invalid user data")
1517
ErrUserNotFound = errors.New("user not found")
1618
ErrInvalidPassword = errors.New("invalid password")
19+
ErrOTPNotFound = errors.New("OTP not found")
1720
)
1821

1922
type UserDao struct {
@@ -240,3 +243,59 @@ func (d *UserDao) DeleteUserById(c context.Context, id int64) error {
240243
}
241244
return nil
242245
}
246+
247+
func (d *UserDao) GenerateOTP(c context.Context, username string) (api.OTPResponse, error) {
248+
user, err := d.GetUserByUsername(c, username)
249+
if err != nil {
250+
return api.OTPResponse{}, fmt.Errorf("failed to get user: %w", err)
251+
}
252+
253+
// Create OTP
254+
otpCode, exp_date, err := otp.GenerateOTP()
255+
if err != nil {
256+
return api.OTPResponse{}, fmt.Errorf("failed to generate OTP: %w", err)
257+
}
258+
query := "INSERT INTO otps (user_id, otp_code, expires_at) VALUES ($1, $2, $3) RETURNING otp_id"
259+
row := d.db.QueryRow(c, query, user.Id, otpCode, exp_date)
260+
var otpId int64
261+
err = row.Scan(&otpId)
262+
if err != nil {
263+
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
264+
return api.OTPResponse{}, ErrUserAlreadyExists
265+
}
266+
return api.OTPResponse{}, fmt.Errorf("failed to insert OTP: %w", err)
267+
}
268+
return api.OTPResponse{
269+
Otp: otpCode,
270+
ValidUntil: exp_date,
271+
}, nil
272+
}
273+
274+
func (d *UserDao) ValidateOTP(c context.Context, otpCode string) (bool, error) {
275+
// Check for currently valid OTP
276+
query := `
277+
SELECT otp_id, user_id, expires_at FROM otps
278+
WHERE otp_code = $1 AND expires_at > CURRENT_TIMESTAMP
279+
ORDER BY created_at DESC LIMIT 1
280+
`
281+
var otpID int64
282+
var userID int64
283+
var expiresAt time.Time
284+
285+
row := d.db.QueryRow(c, query, otpCode)
286+
if err := row.Scan(&otpID, &userID, &expiresAt); err != nil {
287+
if strings.Contains(err.Error(), "no rows in result set") {
288+
return false, ErrOTPNotFound
289+
}
290+
return false, fmt.Errorf("failed to validate OTP: %w", err)
291+
}
292+
293+
// Delete already used OTP
294+
deleteQuery := "DELETE FROM otps WHERE otp_id = $1"
295+
_, err := d.db.Exec(c, deleteQuery, otpID)
296+
if err != nil {
297+
fmt.Printf("Failed to delete used OTP: %v\n", err)
298+
}
299+
300+
return true, nil
301+
}

src/db/postgres_schema_v1.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,13 @@ CREATE TABLE IF NOT EXISTS users (
77
email TEXT NOT NULL UNIQUE,
88
password TEXT NOT NULL,
99
role TEXT NOT NULL CHECK (role IN ('driver', 'controller', 'admin', 'superuser'))
10+
);
11+
12+
-- OTP table
13+
CREATE TABLE IF NOT EXISTS otps (
14+
otp_id SERIAL PRIMARY KEY,
15+
user_id INTEGER NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
16+
otp_code TEXT NOT NULL,
17+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
18+
expires_at TIMESTAMP NOT NULL
1019
);

src/handlers/session.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,45 @@ func (h *SessionHandlers) GetSession(c *gin.Context) {
234234
}
235235
c.JSON(http.StatusOK, sessionResponse)
236236
}
237+
238+
func (h *SessionHandlers) GenerateOTP(c *gin.Context) {
239+
user, userRole, err := getLoggedUser(c, h.dao)
240+
if err != nil || user == nil {
241+
return
242+
}
243+
244+
if userRole != api.UserRequestRoleSuperuser && userRole != api.UserRequestRoleAdmin {
245+
c.JSON(http.StatusForbidden, gin.H{"error": "Only admins can generate OTP"})
246+
return
247+
}
248+
249+
otp, err := h.dao.GenerateOTP(c.Request.Context(), user.Username)
250+
if err != nil {
251+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate OTP"})
252+
return
253+
}
254+
c.JSON(http.StatusOK, otp)
255+
}
256+
257+
func (h *SessionHandlers) ValidateOTP(c *gin.Context) {
258+
var otpRequest api.OTPValidationRequest
259+
if err := c.ShouldBindJSON(&otpRequest); err != nil {
260+
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
261+
return
262+
}
263+
if otpRequest.Otp == "" {
264+
c.JSON(http.StatusBadRequest, gin.H{"error": "OTP code is required"})
265+
return
266+
}
267+
268+
valid, err := h.dao.ValidateOTP(c.Request.Context(), otpRequest.Otp)
269+
if err != nil {
270+
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to validate OTP"})
271+
return
272+
}
273+
if !valid {
274+
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired OTP"})
275+
return
276+
}
277+
c.JSON(http.StatusOK, gin.H{"message": "OTP validated successfully"})
278+
}

0 commit comments

Comments
 (0)