package main 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 }