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.
192 lines
4.5 KiB
192 lines
4.5 KiB
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"git.chrishayward.xyz/x/users/proto"
|
|
)
|
|
|
|
type usersServer struct {
|
|
proto.UsersServer
|
|
secret *string
|
|
users UserDB
|
|
tokens TokenDB
|
|
resetTokens TokenDB
|
|
}
|
|
|
|
func NewUsersServer(secret *string) proto.UsersServer {
|
|
return &usersServer{
|
|
secret: secret,
|
|
users: newInMemoryUserDB(),
|
|
tokens: newInMemoryTokenDB(),
|
|
resetTokens: newInMemoryTokenDB(),
|
|
}
|
|
}
|
|
|
|
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.
|
|
if u, _ := m.users.FindByEmail(in.Form.Email); u != nil {
|
|
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.
|
|
if err := m.users.Save(&User{
|
|
Email: in.Form.Email,
|
|
Password: string(bytes),
|
|
}); err != nil {
|
|
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.
|
|
user, err := m.users.FindByEmail(in.Form.Email)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 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 token.
|
|
expires := time.Now().AddDate(0, 0, 1)
|
|
token := &Token{
|
|
UserID: user.ID,
|
|
Token: uuid.NewString(),
|
|
Expires: &expires,
|
|
}
|
|
|
|
// Save the token.
|
|
m.tokens.Save(token)
|
|
|
|
// Return the response.
|
|
expiresNano := expires.UnixNano()
|
|
return &proto.LoginResponse{
|
|
Token: &proto.UserToken{
|
|
Token: token.Token,
|
|
Expires: &expiresNano,
|
|
},
|
|
}, 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 token.
|
|
token, err := m.tokens.FindByToken(in.Token.Token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Make sure the token hasn't expired.
|
|
if token.Expires.After(time.Now()) {
|
|
return nil, errors.New("Token is expired.")
|
|
}
|
|
|
|
// Return the user ID.
|
|
return &proto.AuthorizeResponse{
|
|
User: &proto.UserInfo{
|
|
Id: int64(token.UserID),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (m *usersServer) ResetPassword(ctx context.Context, in *proto.ResetPasswordRequest) (*proto.ResetPasswordResponse, error) {
|
|
// Find the user.
|
|
user, err := m.users.FindByEmail(in.Form.Email)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Generate a reset token.
|
|
expires := time.Now().AddDate(0, 0, 1)
|
|
token := &Token{
|
|
UserID: user.ID,
|
|
Token: uuid.NewString(),
|
|
Expires: &expires,
|
|
}
|
|
|
|
// Save the token.
|
|
if err := m.resetTokens.Save(token); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return the response.
|
|
return &proto.ResetPasswordResponse{
|
|
Token: &proto.UserToken{
|
|
Token: token.Token,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (m *usersServer) ChangePassword(ctx context.Context, in *proto.ChangePasswordRequest) (*proto.ChangePasswordResponse, error) {
|
|
// Find the reset token.
|
|
resetToken, err := m.resetTokens.FindByToken(in.Token.Token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Find the user.
|
|
user, err := m.users.FindByID(resetToken.UserID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 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 err := m.users.Save(user); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Expire current token.
|
|
if token, err := m.tokens.FindByUserID(user.ID); token != nil && err == nil {
|
|
expires := time.Now()
|
|
token.Expires = &expires
|
|
_ = m.tokens.Save(token)
|
|
}
|
|
|
|
// Return the response.
|
|
return nil, nil
|
|
}
|