Send password reset email
This commit is contained in:
parent
c7d4c1825f
commit
c793dc4aeb
8 changed files with 465 additions and 41 deletions
|
@ -16,7 +16,7 @@ Players always perceive games from the perspective of player number 1 (black).
|
|||
|
||||
### Client commands
|
||||
|
||||
Clients must send a register command or login command before sending any other commands.
|
||||
Clients must send a register command, reset command or login command before sending any other commands.
|
||||
|
||||
- `register <email> <username> <password>`
|
||||
- Register an account. A valid email address must be provided.
|
||||
|
@ -29,6 +29,10 @@ formatted responses are more easily parsed by computers.
|
|||
- The name of the client must be specified.
|
||||
- Aliases: `rj`
|
||||
|
||||
- `resetpassword <email>`
|
||||
- Request a password reset link via email.
|
||||
- This command always terminates the client with the message "resetpasswordok", even if an account is not found.
|
||||
|
||||
- `login [username] [password]`
|
||||
- Log in. A random username is assigned when none is provided.
|
||||
- Usernames must contain at least one non-numeric character.
|
||||
|
|
|
@ -17,6 +17,9 @@ func main() {
|
|||
wsAddress string
|
||||
tz string
|
||||
dataSource string
|
||||
mailServer string
|
||||
passwordSalt string
|
||||
resetSalt string
|
||||
debug int
|
||||
debugCommands bool
|
||||
rollStatistics bool
|
||||
|
@ -25,6 +28,7 @@ func main() {
|
|||
flag.StringVar(&wsAddress, "ws", "localhost:1338", "WebSocket listen address")
|
||||
flag.StringVar(&tz, "tz", "", "Time zone used when calculating statistics")
|
||||
flag.StringVar(&dataSource, "db", "", "Database data source (postgres://username:password@localhost:5432/database_name")
|
||||
flag.StringVar(&mailServer, "smtp", "", "SMTP server address")
|
||||
flag.IntVar(&debug, "debug", 0, "print debug information and serve pprof on specified port")
|
||||
flag.BoolVar(&debugCommands, "debug-commands", false, "allow players to use restricted commands")
|
||||
flag.BoolVar(&rollStatistics, "statistics", false, "print dice roll statistics and exit")
|
||||
|
@ -34,6 +38,13 @@ func main() {
|
|||
dataSource = os.Getenv("BGAMMON_DB")
|
||||
}
|
||||
|
||||
if mailServer == "" {
|
||||
mailServer = os.Getenv("BGAMMON_SMTP")
|
||||
}
|
||||
|
||||
passwordSalt = os.Getenv("BGAMMON_SALT_PASSWORD")
|
||||
resetSalt = os.Getenv("BGAMMON_SALT_RESET")
|
||||
|
||||
if rollStatistics {
|
||||
printRollStatistics()
|
||||
return
|
||||
|
@ -49,7 +60,7 @@ func main() {
|
|||
}()
|
||||
}
|
||||
|
||||
s := server.NewServer(tz, dataSource, false, debugCommands)
|
||||
s := server.NewServer(tz, dataSource, mailServer, passwordSalt, resetSalt, false, debugCommands)
|
||||
if tcpAddress != "" {
|
||||
s.Listen("tcp", tcpAddress)
|
||||
}
|
||||
|
|
43
command.go
43
command.go
|
@ -3,27 +3,28 @@ package bgammon
|
|||
type Command string
|
||||
|
||||
const (
|
||||
CommandLogin = "login" // Log in with username and password, or as a guest.
|
||||
CommandLoginJSON = "loginjson" // Log in with username and password, or as a guest, and enable JSON messages.
|
||||
CommandRegister = "register" // Register an account.
|
||||
CommandRegisterJSON = "registerjson" // Register an account and enable JSON messages.
|
||||
CommandHelp = "help" // Print help information.
|
||||
CommandJSON = "json" // Enable or disable JSON formatted messages.
|
||||
CommandSay = "say" // Send chat message.
|
||||
CommandList = "list" // List available matches.
|
||||
CommandCreate = "create" // Create match.
|
||||
CommandJoin = "join" // Join match.
|
||||
CommandLeave = "leave" // Leave match.
|
||||
CommandDouble = "double" // Offer double to opponent.
|
||||
CommandResign = "resign" // Decline double offer and resign game.
|
||||
CommandRoll = "roll" // Roll dice.
|
||||
CommandMove = "move" // Move checkers.
|
||||
CommandReset = "reset" // Reset checker movement.
|
||||
CommandOk = "ok" // Confirm checker movement and pass turn to next player.
|
||||
CommandRematch = "rematch" // Confirm checker movement and pass turn to next player.
|
||||
CommandBoard = "board" // Print current board state in human-readable form.
|
||||
CommandPong = "pong" // Response to server ping.
|
||||
CommandDisconnect = "disconnect" // Disconnect from server.
|
||||
CommandLogin = "login" // Log in with username and password, or as a guest.
|
||||
CommandLoginJSON = "loginjson" // Log in with username and password, or as a guest, and enable JSON messages.
|
||||
CommandRegister = "register" // Register an account.
|
||||
CommandRegisterJSON = "registerjson" // Register an account and enable JSON messages.
|
||||
CommandResetPassword = "resetpassword" // Request password reset link via email.
|
||||
CommandHelp = "help" // Print help information.
|
||||
CommandJSON = "json" // Enable or disable JSON formatted messages.
|
||||
CommandSay = "say" // Send chat message.
|
||||
CommandList = "list" // List available matches.
|
||||
CommandCreate = "create" // Create match.
|
||||
CommandJoin = "join" // Join match.
|
||||
CommandLeave = "leave" // Leave match.
|
||||
CommandDouble = "double" // Offer double to opponent.
|
||||
CommandResign = "resign" // Decline double offer and resign game.
|
||||
CommandRoll = "roll" // Roll dice.
|
||||
CommandMove = "move" // Move checkers.
|
||||
CommandReset = "reset" // Reset checker movement.
|
||||
CommandOk = "ok" // Confirm checker movement and pass turn to next player.
|
||||
CommandRematch = "rematch" // Confirm checker movement and pass turn to next player.
|
||||
CommandBoard = "board" // Print current board state in human-readable form.
|
||||
CommandPong = "pong" // Response to server ping.
|
||||
CommandDisconnect = "disconnect" // Disconnect from server.
|
||||
)
|
||||
|
||||
type EventType string
|
||||
|
|
24
go.mod
24
go.mod
|
@ -1,19 +1,41 @@
|
|||
module code.rocket9labs.com/tslocum/bgammon
|
||||
|
||||
go 1.20
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/gobwas/ws v1.3.1
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/jackc/pgx/v5 v5.5.1
|
||||
github.com/matcornic/hermes/v2 v2.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver v1.5.0 // indirect
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
|
||||
github.com/PuerkitoBio/goquery v1.8.1 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||
github.com/vanng822/css v1.0.1 // indirect
|
||||
github.com/vanng822/go-premailer v1.20.2 // indirect
|
||||
golang.org/x/crypto v0.16.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
)
|
||||
|
|
95
go.sum
95
go.sum
|
@ -1,13 +1,48 @@
|
|||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
|
||||
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
|
||||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
||||
|
@ -15,36 +50,91 @@ github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZ
|
|||
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
|
||||
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/matcornic/hermes/v2 v2.1.0 h1:9TDYFBPFv6mcXanaDmRDEp/RTWj0dTTi+LpFnnnfNWc=
|
||||
github.com/matcornic/hermes/v2 v2.1.0/go.mod h1:2+ziJeoyRfaLiATIL8VZ7f9hpzH4oDHqTmn0bhrsgVI=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
|
||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/unrolled/render v1.0.3/go.mod h1:gN9T0NhL4Bfbwu8ann7Ry/TGHYfosul+J0obPf6NBdM=
|
||||
github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w=
|
||||
github.com/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8=
|
||||
github.com/vanng822/css v1.0.1/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w=
|
||||
github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe/go.mod h1:JTFJA/t820uFDoyPpErFQ3rb3amdZoPtxcKervG0OE4=
|
||||
github.com/vanng822/go-premailer v1.20.2 h1:vKs4VdtfXDqL7IXC2pkiBObc1bXM9bYH3Wa+wYw2DnI=
|
||||
github.com/vanng822/go-premailer v1.20.2/go.mod h1:RAxbRFp6M/B171gsKu8dsyq+Y5NGsUUvYfg+WQWusbE=
|
||||
github.com/vanng822/r2router v0.0.0-20150523112421-1023140a4f30/go.mod h1:1BVq8p2jVr55Ost2PkZWDrG86PiJ/0lxqcXoAcGxvWU=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20181029175232-7e6ffbd03851/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190225065934-cc5685c2db12/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
|
@ -52,10 +142,12 @@ golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
|
@ -68,5 +160,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -5,23 +5,34 @@ package server
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"mime/multipart"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.rocket9labs.com/tslocum/bgammon"
|
||||
"github.com/alexedwards/argon2id"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/matcornic/hermes/v2"
|
||||
)
|
||||
|
||||
const databaseSchema = `
|
||||
CREATE TABLE account (
|
||||
id serial PRIMARY KEY,
|
||||
created bigint NOT NULL,
|
||||
active bigint NOT NULL,
|
||||
email text NOT NULL,
|
||||
username text NOT NULL,
|
||||
password text NOT NULL
|
||||
id serial PRIMARY KEY,
|
||||
created bigint NOT NULL,
|
||||
confirmed bigint NOT NULL DEFAULT 0,
|
||||
active bigint NOT NULL,
|
||||
reset bigint NOT NULL DEFAULT 0,
|
||||
email text NOT NULL,
|
||||
username text NOT NULL,
|
||||
password text NOT NULL
|
||||
);
|
||||
CREATE TABLE game (
|
||||
id serial PRIMARY KEY,
|
||||
|
@ -142,11 +153,149 @@ func registerAccount(a *account) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func resetAccount(mailServer string, resetSalt string, email []byte) error {
|
||||
if db == nil {
|
||||
return nil
|
||||
} else if len(bytes.TrimSpace(email)) == 0 {
|
||||
return fmt.Errorf("please enter an email address")
|
||||
}
|
||||
|
||||
tx, err := begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Commit(context.Background())
|
||||
|
||||
var result int
|
||||
err = tx.QueryRow(context.Background(), "SELECT COUNT(*) FROM account WHERE email = $1", bytes.ToLower(bytes.TrimSpace(email))).Scan(&result)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if result == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
id int
|
||||
reset int64
|
||||
accountEmail []byte
|
||||
passwordHash []byte
|
||||
)
|
||||
err = tx.QueryRow(context.Background(), "SELECT id, reset, email, password FROM account WHERE email = $1", bytes.ToLower(bytes.TrimSpace(email))).Scan(&id, &reset, &accountEmail, &passwordHash)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if id == 0 || len(passwordHash) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
const resetTimeout = 86400 // 24 hours.
|
||||
if time.Now().Unix()-reset >= resetTimeout {
|
||||
timestamp := time.Now().Unix()
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(fmt.Sprintf("%d", timestamp) + resetSalt))
|
||||
hash := fmt.Sprintf("%x", h.Sum(nil))[0:16]
|
||||
|
||||
emailConfig := hermes.Hermes{
|
||||
Product: hermes.Product{
|
||||
Name: "https://bgammon.org",
|
||||
Link: " ",
|
||||
Copyright: " ",
|
||||
},
|
||||
}
|
||||
|
||||
resetEmail := hermes.Email{
|
||||
Body: hermes.Body{
|
||||
Greeting: "Hello",
|
||||
Intros: []string{
|
||||
"You are receiving this email because you (or someone else) requested to reset your bgammon.org password.",
|
||||
},
|
||||
Actions: []hermes.Action{
|
||||
{
|
||||
Instructions: "Click to reset your password:",
|
||||
Button: hermes.Button{
|
||||
Color: "#DC4D2F",
|
||||
Text: "Reset your password",
|
||||
Link: "https://bgammon.org/reset/" + strconv.Itoa(id) + "/" + hash,
|
||||
},
|
||||
},
|
||||
},
|
||||
Outros: []string{
|
||||
"If you did not request to reset your bgammon.org password, no further action is required on your part.",
|
||||
},
|
||||
Signature: "Ciao",
|
||||
},
|
||||
}
|
||||
emailPlain, err := emailConfig.GeneratePlainText(resetEmail)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
emailPlain = strings.ReplaceAll(emailPlain, "https://bgammon.org -", "https://bgammon.org")
|
||||
|
||||
emailHTML, err := emailConfig.GenerateHTML(resetEmail)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if sendEmail(mailServer, string(accountEmail), "Reset your bgammon.org password", emailPlain, emailHTML) {
|
||||
_, err = tx.Exec(context.Background(), "UPDATE account SET reset = $1 WHERE id = $2", timestamp, id)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func confirmResetAccount(resetSalt string, id int, key string) (string, error) {
|
||||
if db == nil {
|
||||
return "", nil
|
||||
} else if id == 0 {
|
||||
return "", fmt.Errorf("no id provided")
|
||||
} else if len(strings.TrimSpace(key)) == 0 {
|
||||
return "", fmt.Errorf("no reset key provided")
|
||||
}
|
||||
|
||||
tx, err := begin()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer tx.Commit(context.Background())
|
||||
|
||||
var result int
|
||||
err = tx.QueryRow(context.Background(), "SELECT COUNT(*) FROM account WHERE id = $1 AND reset != 0", id).Scan(&result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if result == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var reset int
|
||||
err = tx.QueryRow(context.Background(), "SELECT reset FROM account WHERE id = $1", id).Scan(&reset)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(fmt.Sprintf("%d", reset) + resetSalt))
|
||||
hash := fmt.Sprintf("%x", h.Sum(nil))[0:16]
|
||||
if key != hash {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
newPassword := randomAlphanumeric(7)
|
||||
|
||||
passwordHash, err := argon2id.CreateHash(newPassword, passwordArgon2id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(context.Background(), "UPDATE account SET password = $1, reset = reset - 1 WHERE id = $2", passwordHash, id)
|
||||
return newPassword, err
|
||||
}
|
||||
|
||||
func loginAccount(username []byte, password []byte) (*account, error) {
|
||||
if db == nil {
|
||||
return nil, nil
|
||||
} else if len(bytes.TrimSpace(username)) == 0 {
|
||||
return nil, fmt.Errorf("please enter an email address")
|
||||
return nil, fmt.Errorf("please enter a username")
|
||||
} else if len(bytes.TrimSpace(password)) == 0 {
|
||||
return nil, fmt.Errorf("please enter a password")
|
||||
}
|
||||
|
@ -325,3 +474,92 @@ func botStats(name string, tz *time.Location) (*botStatsResult, error) {
|
|||
func midnight(t time.Time) time.Time {
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
|
||||
}
|
||||
|
||||
func sendEmail(mailServer string, emailAddress string, emailSubject string, emailPlain string, emailHTML string) bool {
|
||||
mixedContent := &bytes.Buffer{}
|
||||
mixedWriter := multipart.NewWriter(mixedContent)
|
||||
var newBoundary = "RELATED-" + mixedWriter.Boundary()
|
||||
mixedWriter.SetBoundary(first70("MIXED-" + mixedWriter.Boundary()))
|
||||
relatedWriter, newBoundary := nestedMultipart(mixedWriter, "multipart/related", newBoundary)
|
||||
altWriter, newBoundary := nestedMultipart(relatedWriter, "multipart/alternative", "ALTERNATIVE-"+newBoundary)
|
||||
|
||||
var childContent io.Writer
|
||||
childContent, _ = altWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {"text/plain"}})
|
||||
childContent.Write([]byte(emailPlain))
|
||||
childContent, _ = altWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {"text/html"}})
|
||||
childContent.Write([]byte(emailHTML))
|
||||
|
||||
altWriter.Close()
|
||||
relatedWriter.Close()
|
||||
mixedWriter.Close()
|
||||
|
||||
if mailServer == "" {
|
||||
fmt.Print(`From: bgammon.org <noreply@bgammon.org>
|
||||
To: <` + emailAddress + `>
|
||||
Subject: ` + emailSubject + `
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed; boundary=`)
|
||||
fmt.Print(mixedWriter.Boundary(), "\n\n")
|
||||
fmt.Println(mixedContent.String())
|
||||
return true
|
||||
}
|
||||
|
||||
c, err := smtp.Dial(mailServer)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
c.Mail("noreply@bgammon.org")
|
||||
c.Rcpt(emailAddress)
|
||||
|
||||
wc, err := c.Data()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer wc.Close()
|
||||
|
||||
fmt.Fprint(wc, `From: bgammon.org <noreply@bgammon.org>
|
||||
To: `+emailAddress+`
|
||||
Subject: `+emailSubject+`
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed; boundary=`)
|
||||
fmt.Fprint(wc, mixedWriter.Boundary(), "\n\n")
|
||||
fmt.Fprintln(wc, mixedContent.String())
|
||||
return true
|
||||
}
|
||||
|
||||
func nestedMultipart(enclosingWriter *multipart.Writer, contentType, boundary string) (nestedWriter *multipart.Writer, newBoundary string) {
|
||||
|
||||
var contentBuffer io.Writer
|
||||
var err error
|
||||
|
||||
boundary = first70(boundary)
|
||||
contentWithBoundary := contentType + "; boundary=\"" + boundary + "\""
|
||||
contentBuffer, err = enclosingWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {contentWithBoundary}})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
nestedWriter = multipart.NewWriter(contentBuffer)
|
||||
newBoundary = nestedWriter.Boundary()
|
||||
nestedWriter.SetBoundary(boundary)
|
||||
return
|
||||
}
|
||||
|
||||
func first70(str string) string {
|
||||
if len(str) > 70 {
|
||||
return string(str[0:69])
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
var letters = []rune("abcdefghkmnpqrstwxyzABCDEFGHJKMNPQRSTWXYZ23456789")
|
||||
|
||||
func randomAlphanumeric(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
|
|
@ -23,6 +23,14 @@ func registerAccount(a *account) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func resetAccount(mailServer string, resetSalt string, email []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func confirmResetAccount(resetSalt string, id int, key string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func loginAccount(username []byte, password []byte) (*account, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"time"
|
||||
|
||||
"code.rocket9labs.com/tslocum/bgammon"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const clientTimeout = 40 * time.Second
|
||||
|
@ -49,18 +50,25 @@ type server struct {
|
|||
gamesCacheTime time.Time
|
||||
gamesCacheLock sync.Mutex
|
||||
|
||||
mailServer string
|
||||
passwordSalt string
|
||||
resetSalt string
|
||||
|
||||
tz *time.Location
|
||||
|
||||
relayChat bool // Chats are not relayed normally. This option is only used by local servers.
|
||||
}
|
||||
|
||||
func NewServer(tz string, dataSource string, relayChat bool, allowDebug bool) *server {
|
||||
func NewServer(tz string, dataSource string, mailServer string, passwordSalt string, resetSalt string, relayChat bool, allowDebug bool) *server {
|
||||
const bufferSize = 10
|
||||
s := &server{
|
||||
newGameIDs: make(chan int),
|
||||
newClientIDs: make(chan int),
|
||||
commands: make(chan serverCommand, bufferSize),
|
||||
welcome: []byte("hello Welcome to bgammon.org! Please log in by sending the 'login' command. You may specify a username, otherwise you will be assigned a random username. If you specify a username, you may also specify a password. Have fun!"),
|
||||
mailServer: mailServer,
|
||||
passwordSalt: passwordSalt,
|
||||
resetSalt: resetSalt,
|
||||
relayChat: relayChat,
|
||||
}
|
||||
|
||||
|
@ -132,6 +140,27 @@ func (s *server) cachedMatches() []byte {
|
|||
return s.gamesCache
|
||||
}
|
||||
|
||||
func (s *server) handleResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil || id <= 0 {
|
||||
return
|
||||
}
|
||||
key := vars["key"]
|
||||
|
||||
newPassword, err := confirmResetAccount(s.resetSalt, id, key)
|
||||
if err != nil {
|
||||
log.Printf("failed to reset password: %s", err)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
if err != nil || newPassword == "" {
|
||||
w.Write([]byte(`<!DOCTYPE html><html><body><h1>Invalid or expired password reset link.</h1></body></html>`))
|
||||
return
|
||||
}
|
||||
w.Write([]byte(`<!DOCTYPE html><html><body><h1>Your bgammon.org password has been reset.</h1>Your new password is <b>` + newPassword + `</b></body></html>`))
|
||||
}
|
||||
|
||||
func (s *server) handleListMatches(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(s.cachedMatches())
|
||||
|
@ -205,14 +234,15 @@ func (s *server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|||
func (s *server) listenWebSocket(address string) {
|
||||
log.Printf("Listening for WebSocket connections on %s...", address)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/matches", s.handleListMatches)
|
||||
mux.HandleFunc("/stats", s.handlePrintStats)
|
||||
mux.HandleFunc("/stats-tabula", s.handlePrintTabulaStats)
|
||||
mux.HandleFunc("/stats-wildbg", s.handlePrintWildBGStats)
|
||||
mux.HandleFunc("/", s.handleWebSocket)
|
||||
m := mux.NewRouter()
|
||||
m.HandleFunc("/reset/{id:[0-9]+}/{key:[A-Za-z0-9]+}", s.handleResetPassword)
|
||||
m.HandleFunc("/matches", s.handleListMatches)
|
||||
m.HandleFunc("/stats", s.handlePrintStats)
|
||||
m.HandleFunc("/stats-tabula", s.handlePrintTabulaStats)
|
||||
m.HandleFunc("/stats-wildbg", s.handlePrintWildBGStats)
|
||||
m.HandleFunc("/", s.handleWebSocket)
|
||||
|
||||
err := http.ListenAndServe(address, mux)
|
||||
err := http.ListenAndServe(address, m)
|
||||
log.Fatalf("failed to listen on %s: %s", address, err)
|
||||
}
|
||||
|
||||
|
@ -467,6 +497,21 @@ COMMANDS:
|
|||
|
||||
// Require users to send login command first.
|
||||
if cmd.client.account == -1 {
|
||||
resetCommand := keyword == bgammon.CommandResetPassword
|
||||
if resetCommand {
|
||||
if len(params) > 0 {
|
||||
email := bytes.ToLower(bytes.TrimSpace(params[0]))
|
||||
if len(email) > 0 {
|
||||
err := resetAccount(s.mailServer, s.resetSalt, email)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to reset password: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd.client.Terminate("resetpasswordok")
|
||||
continue
|
||||
}
|
||||
|
||||
loginCommand := keyword == bgammon.CommandLogin || keyword == bgammon.CommandLoginJSON || keyword == "lj"
|
||||
registerCommand := keyword == bgammon.CommandRegister || keyword == bgammon.CommandRegisterJSON || keyword == "rj"
|
||||
if loginCommand || registerCommand {
|
||||
|
@ -508,7 +553,7 @@ COMMANDS:
|
|||
a := &account{
|
||||
email: email,
|
||||
username: username,
|
||||
password: password,
|
||||
password: append(password, []byte(s.passwordSalt)...),
|
||||
}
|
||||
err := registerAccount(a)
|
||||
if err != nil {
|
||||
|
@ -556,7 +601,7 @@ COMMANDS:
|
|||
}
|
||||
|
||||
if len(password) > 0 {
|
||||
a, err := loginAccount(username, password)
|
||||
a, err := loginAccount(username, append(password, []byte(s.passwordSalt)...))
|
||||
if err != nil {
|
||||
cmd.client.Terminate(fmt.Sprintf("Failed to log in: %s", err))
|
||||
continue
|
||||
|
|
Loading…
Reference in a new issue