You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

198 lines
5.0 KiB

package server
import (
"context"
"errors"
"log"
"time"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"git.chrishayward.xyz/x/users/proto"
"git.chrishayward.xyz/x/users/server/models"
)
type usersServer struct {
proto.UsersServer
secret *string
db *gorm.DB
}
func NewUsersServer(secret *string, db *gorm.DB) proto.UsersServer {
db.AutoMigrate(&models.User{}, &models.Session{}, &models.PasswordToken{})
return &usersServer{
secret: secret,
db: db,
}
}
func (m *usersServer) Register(ctx context.Context, in *proto.RegisterRequest) (*proto.RegisterResponse, error) {
// Make sure both passwords are included and match.
if in.Form.Password == nil || in.Form.PasswordAgain == nil {
return nil, errors.New("Must include password(s).")
}
if *in.Form.Password != *in.Form.PasswordAgain {
return nil, errors.New("Passwords do not match.")
}
// Check for an existing user.
var user models.User
tx := m.db.First(&user, "email = ?", in.Form.Email)
if tx.RowsAffected != 0 {
return nil, errors.New("User already exists.")
}
// Encode the password.
bytes, err := bcrypt.GenerateFromPassword([]byte(*in.Form.Password), bcrypt.MaxCost)
if err != nil {
log.Fatalf("Failed to encode password: %v", err)
return nil, errors.New("Failed to encode password.")
}
// Create the new user.
user.Email = in.Form.Email
user.Password = string(bytes)
tx = m.db.Create(&user)
if tx.RowsAffected == 0 {
log.Fatalf("Failed to save user: %v", err)
return nil, errors.New("Failed to save user.")
}
// Return the response.
return &proto.RegisterResponse{}, nil
}
func (m *usersServer) Login(ctx context.Context, in *proto.LoginRequest) (*proto.LoginResponse, error) {
// Make sure the password is included.
if in.Form.Password == nil {
return nil, errors.New("Password must be included.")
}
// Find the user.
var user models.User
tx := m.db.First(&user, "email = ?", in.Form.Email)
if tx.RowsAffected == 0 {
return nil, errors.New("User not found.")
}
// Compare the passwords.
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(*in.Form.Password)); err != nil {
return nil, errors.New("Passwords do not match.")
}
// Create a session.
session := &models.Session{
Token: uuid.NewString(),
Expires: time.Now().AddDate(0, 0, 1).UnixNano(),
UserID: user.ID,
}
// Save the token.
tx = m.db.Create(&session)
if tx.RowsAffected == 0 {
return nil, errors.New("Failed to create session.")
}
// Return the response.
return &proto.LoginResponse{
Token: &proto.UserToken{
Token: session.Token,
Expires: &session.Expires,
},
}, nil
}
func (m *usersServer) Authorize(ctx context.Context, in *proto.AuthorizeRequest) (*proto.AuthorizeResponse, error) {
// Make sure the secrets match.
if in.Secret != *m.secret {
return nil, errors.New("Secrets do not match.")
}
// Find the session.
var session models.Session
tx := m.db.First(&session, "token = ?", in.Token.Token)
if tx.RowsAffected == 0 {
return nil, errors.New("Session not found.")
}
// Make sure the session hasn't expired.
if time.Now().UnixNano() > session.Expires {
return nil, errors.New("Token is expired.")
}
// Return the user ID.
return &proto.AuthorizeResponse{
User: &proto.UserInfo{
Id: int64(session.UserID),
},
}, nil
}
func (m *usersServer) ResetPassword(ctx context.Context, in *proto.ResetPasswordRequest) (*proto.ResetPasswordResponse, error) {
// Find the user.
var user models.User
tx := m.db.First(&user, "email = ?", in.Form.Email)
if tx.RowsAffected == 0 {
return nil, errors.New("User not found.")
}
// Generate a reset token.
resetToken := &models.PasswordToken{
UserID: user.ID,
Token: uuid.NewString(),
Expires: time.Now().UnixNano(),
}
// Save the token.
tx = m.db.Create(resetToken)
if tx.RowsAffected == 0 {
return nil, errors.New("Failed to create token.")
}
// Return the response.
return &proto.ResetPasswordResponse{
Token: &proto.UserToken{
Token: resetToken.Token,
},
}, nil
}
func (m *usersServer) ChangePassword(ctx context.Context, in *proto.ChangePasswordRequest) (*proto.ChangePasswordResponse, error) {
// Find the reset token.
var resetToken models.PasswordToken
tx := m.db.First(&resetToken, "token = ?", in.Token.Token)
if tx.RowsAffected == 0 {
return nil, errors.New("Token not found.")
}
// Find the user.
var user models.User
tx = m.db.First(&user, "id = ?", resetToken.UserID)
if tx.RowsAffected == 0 {
return nil, errors.New("User not found.")
}
// Update the password.
bytes, err := bcrypt.GenerateFromPassword([]byte(*in.Form.Password), bcrypt.MaxCost)
if err != nil {
log.Fatalf("Failed to encode password: %v", err)
return nil, errors.New("Failed to encode password.")
}
user.Password = string(bytes)
if tx = m.db.Save(user); tx.RowsAffected == 0 {
return nil, errors.New("Failed to update password.")
}
// Expire current token.
resetToken.Expires = time.Now().UnixNano()
if tx = m.db.Save(&resetToken); tx.RowsAffected == 0 {
return nil, errors.New("Failed to update password.")
}
// Return the response.
return nil, nil
}