Christopher James Hayward
1 year ago
9 changed files with 531 additions and 114 deletions
-
39cmd/server/main.go
-
192cmd/server/server.go
-
60cmd/server/token_db.go
-
62cmd/server/user_db.go
-
15go.mod
-
24go.sum
-
184proto/users.pb.go
-
15proto/users.proto
-
52proto/users_grpc.pb.go
@ -0,0 +1,39 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"flag" |
||||
|
"fmt" |
||||
|
"log" |
||||
|
"net" |
||||
|
|
||||
|
"git.chrishayward.xyz/x/users/proto" |
||||
|
"github.com/google/uuid" |
||||
|
"google.golang.org/grpc" |
||||
|
) |
||||
|
|
||||
|
var ( |
||||
|
secretDefault = uuid.NewString() |
||||
|
secret = flag.String("secret", secretDefault, "--secret=SECRET") |
||||
|
port = flag.Uint("port", 8080, "--port=8080") |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
// Parse the optional flags.
|
||||
|
flag.Parse() |
||||
|
|
||||
|
// If the secret has not been set print it to the console.
|
||||
|
if *secret == secretDefault { |
||||
|
fmt.Printf("SECRET=%s", secretDefault) |
||||
|
} |
||||
|
|
||||
|
// Create the network listener.
|
||||
|
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) |
||||
|
if err != nil { |
||||
|
log.Fatalf("Failed to listen: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Start listening for requests.
|
||||
|
srv := grpc.NewServer() |
||||
|
proto.RegisterUsersServer(srv, newUsersServer(secret)) |
||||
|
srv.Serve(lis) |
||||
|
} |
@ -0,0 +1,192 @@ |
|||||
|
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 |
||||
|
} |
@ -0,0 +1,60 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
type Token struct { |
||||
|
UserID int64 |
||||
|
Token string |
||||
|
Expires *time.Time |
||||
|
} |
||||
|
|
||||
|
type TokenDB interface { |
||||
|
FindByUserID(id int64) (*Token, error) |
||||
|
FindByToken(token string) (*Token, error) |
||||
|
Save(token *Token) error |
||||
|
} |
||||
|
|
||||
|
type inMemoryTokenDB struct { |
||||
|
tokens []*Token |
||||
|
} |
||||
|
|
||||
|
func newInMemoryTokenDB() *inMemoryTokenDB { |
||||
|
return &inMemoryTokenDB{ |
||||
|
tokens: make([]*Token, 0), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (m *inMemoryTokenDB) FindByUserID(id int64) (*Token, error) { |
||||
|
for _, t := range m.tokens { |
||||
|
if t.UserID == id { |
||||
|
return t, nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil, errors.New("Token not found.") |
||||
|
} |
||||
|
|
||||
|
func (m *inMemoryTokenDB) FindByToken(token string) (*Token, error) { |
||||
|
for _, t := range m.tokens { |
||||
|
if t.Token == token { |
||||
|
return t, nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil, errors.New("Token not found.") |
||||
|
} |
||||
|
|
||||
|
func (m *inMemoryTokenDB) Save(token *Token) error { |
||||
|
for i, t := range m.tokens { |
||||
|
if t.UserID == token.UserID || t.Token == token.Token { |
||||
|
m.tokens[i] = token |
||||
|
return nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
m.tokens = append(m.tokens, token) |
||||
|
return nil |
||||
|
} |
@ -0,0 +1,62 @@ |
|||||
|
package main |
||||
|
|
||||
|
import "errors" |
||||
|
|
||||
|
type User struct { |
||||
|
ID int64 |
||||
|
Email string |
||||
|
Password string |
||||
|
} |
||||
|
|
||||
|
type UserDB interface { |
||||
|
FindByID(id int64) (*User, error) |
||||
|
FindByEmail(email string) (*User, error) |
||||
|
Save(*User) error |
||||
|
} |
||||
|
|
||||
|
type inMemoryUserDB struct { |
||||
|
UserDB |
||||
|
nextID int |
||||
|
users []*User |
||||
|
} |
||||
|
|
||||
|
func newInMemoryUserDB() *inMemoryUserDB { |
||||
|
return &inMemoryUserDB{ |
||||
|
nextID: 1, |
||||
|
users: make([]*User, 0), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (m *inMemoryUserDB) FindByID(id int64) (*User, error) { |
||||
|
for _, u := range m.users { |
||||
|
if u.ID == id { |
||||
|
return u, nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil, errors.New("User not found.") |
||||
|
} |
||||
|
|
||||
|
func (m *inMemoryUserDB) FindByEmail(email string) (*User, error) { |
||||
|
for _, u := range m.users { |
||||
|
if u.Email == email { |
||||
|
return u, nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil, errors.New("User not found.") |
||||
|
} |
||||
|
|
||||
|
func (m *inMemoryUserDB) Save(user *User) error { |
||||
|
for i, u := range m.users { |
||||
|
if u.ID == user.ID || u.Email == user.Email { |
||||
|
m.users[i] = user |
||||
|
return nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
user.ID = m.nextID |
||||
|
m.users = append(m.users, user) |
||||
|
m.nextID++ |
||||
|
return nil |
||||
|
} |
@ -1,3 +1,18 @@ |
|||||
module git.chrishayward.xyz/x/users |
module git.chrishayward.xyz/x/users |
||||
|
|
||||
go 1.20 |
go 1.20 |
||||
|
|
||||
|
require ( |
||||
|
github.com/google/uuid v1.3.0 |
||||
|
golang.org/x/crypto v0.10.0 |
||||
|
google.golang.org/grpc v1.56.1 |
||||
|
google.golang.org/protobuf v1.31.0 |
||||
|
) |
||||
|
|
||||
|
require ( |
||||
|
github.com/golang/protobuf v1.5.3 // indirect |
||||
|
golang.org/x/net v0.10.0 // indirect |
||||
|
golang.org/x/sys v0.9.0 // indirect |
||||
|
golang.org/x/text v0.10.0 // indirect |
||||
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect |
||||
|
) |
@ -0,0 +1,24 @@ |
|||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= |
||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= |
||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= |
||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= |
||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= |
||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= |
||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= |
||||
|
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= |
||||
|
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= |
||||
|
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= |
||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= |
||||
|
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= |
||||
|
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
|
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= |
||||
|
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= |
||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= |
||||
|
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= |
||||
|
google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ= |
||||
|
google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= |
||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= |
||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= |
||||
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= |
||||
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= |
Write
Preview
Loading…
Cancel
Save
Reference in new issue