440 lines
13 KiB
Go
440 lines
13 KiB
Go
/*
|
|
* Amsterdam Web Communities System
|
|
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
*/
|
|
// Package main contains the high-level Amsterdam logic.
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"time"
|
|
|
|
"git.erbosoft.com/amy/amsterdam/database"
|
|
"git.erbosoft.com/amy/amsterdam/email"
|
|
"git.erbosoft.com/amy/amsterdam/ui"
|
|
"git.erbosoft.com/amy/amsterdam/util"
|
|
"github.com/labstack/gommon/log"
|
|
)
|
|
|
|
/* LoginForm renders the Amsterdam login form.
|
|
* Parameters:
|
|
* ctxt - The AmContext for the request.
|
|
* Returns:
|
|
* Command string dictating what to be rendered.
|
|
* Data as a parameter for the command string.
|
|
* Standard Go error status.
|
|
*/
|
|
func LoginForm(ctxt ui.AmContext) (string, any, error) {
|
|
// Get target URI.
|
|
target := ctxt.Parameter("tgt")
|
|
if target == "" {
|
|
target = "/"
|
|
}
|
|
|
|
// If user is already logged in, this is a no-op.
|
|
if !ctxt.CurrentUser().IsAnon {
|
|
return "redirect", target, nil
|
|
}
|
|
|
|
dlg, err := ui.AmLoadDialog("login")
|
|
if err == nil {
|
|
dlg.Field("tgt").Value = target
|
|
return dlg.Render(ctxt)
|
|
}
|
|
return ui.ErrorPage(ctxt, err)
|
|
}
|
|
|
|
/* Login handles logging in to Amsterdam.
|
|
* Parameters:
|
|
* ctxt - The AmContext for the request.
|
|
* Returns:
|
|
* Command string dictating what to be rendered.
|
|
* Data as a parameter for the command string.
|
|
* Standard Go error status.
|
|
*/
|
|
func Login(ctxt ui.AmContext) (string, any, error) {
|
|
dlg, err := ui.AmLoadDialog("login")
|
|
if err == nil {
|
|
dlg.LoadFromForm(ctxt)
|
|
target := dlg.Field("tgt").Value
|
|
if target == "" {
|
|
target = "/"
|
|
}
|
|
// If user is already logged in, this is a no-op.
|
|
if !ctxt.CurrentUser().IsAnon {
|
|
return "redirect", target, nil
|
|
}
|
|
|
|
action := dlg.WhichButton(ctxt)
|
|
if action == "cancel" { // Cancel button pressed
|
|
return "redirect", target, nil
|
|
}
|
|
if action == "remind" { // Password Reminder button pressed
|
|
user, uerr := database.AmGetUserByName(dlg.Field("user").Value)
|
|
if uerr == nil {
|
|
var ci *database.ContactInfo
|
|
ci, uerr = user.ContactInfo()
|
|
if uerr == nil {
|
|
if ci != nil && ci.Email != nil && *ci.Email != "" {
|
|
pchange := database.AmNewPasswordChangeRequest(user.Uid, user.Username, *ci.Email)
|
|
msg := email.AmNewEmailMessage(ctxt.CurrentUserId(), ctxt.RemoteIP())
|
|
msg.AddTo(*ci.Email, "")
|
|
msg.SetTemplate("pass_remind.jet")
|
|
msg.AddVariable("username", user.Username)
|
|
msg.AddVariable("reminder", user.PassReminder)
|
|
msg.AddVariable("change_uid", user.Uid)
|
|
msg.AddVariable("change_auth", pchange.Authentication)
|
|
msg.Send()
|
|
} else {
|
|
uerr = errors.New("cannot find email address")
|
|
}
|
|
}
|
|
}
|
|
|
|
dlg.Field("pass").Value = ""
|
|
if uerr == nil {
|
|
return dlg.RenderInfo(ctxt, "Password reminder has been sent to your E-mail address.")
|
|
} else {
|
|
return dlg.RenderError(ctxt, uerr.Error())
|
|
}
|
|
}
|
|
if action == "login" { // Login button pressed
|
|
// authenticate the user
|
|
user, uerr := database.AmAuthenticateUser(dlg.Field("user").Value, dlg.Field("pass").Value, ctxt.RemoteIP())
|
|
if uerr != nil {
|
|
dlg.Field("pass").Value = ""
|
|
return dlg.RenderError(ctxt, uerr.Error())
|
|
}
|
|
ctxt.ReplaceUser(user)
|
|
if dlg.Field("saveme").IsChecked() {
|
|
// create and save an authentication token
|
|
authString, cerr := user.NewAuthToken()
|
|
if cerr == nil {
|
|
ctxt.SetLoginCookie(authString)
|
|
|
|
} else {
|
|
log.Errorf("unable to generate auth string for uid %d: %v", user.Uid, cerr)
|
|
}
|
|
}
|
|
if user.VerifyEMail {
|
|
return "redirect", target, nil
|
|
} else {
|
|
return "redirect", "/verify?tgt=" + url.PathEscape(target), nil
|
|
}
|
|
}
|
|
dlg.Field("pass").Value = ""
|
|
return dlg.RenderError(ctxt, "No known button click on POST to login function.")
|
|
}
|
|
return ui.ErrorPage(ctxt, err)
|
|
}
|
|
|
|
/* Logout handles logging out from Amsterdam.
|
|
* Parameters:
|
|
* ctxt - The AmContext for the request.
|
|
* Returns:
|
|
* Command string dictating what to be rendered.
|
|
* Data as a parameter for the command string.
|
|
* Standard Go error status.
|
|
*/
|
|
func Logout(ctxt ui.AmContext) (string, any, error) {
|
|
// Get target URI.
|
|
target := ctxt.Parameter("tgt")
|
|
if target == "" {
|
|
target = "/"
|
|
}
|
|
|
|
if !ctxt.CurrentUser().IsAnon {
|
|
ctxt.ClearLoginCookie()
|
|
ctxt.ClearSession()
|
|
}
|
|
return "redirect", target, nil
|
|
}
|
|
|
|
/* VerifyEmailForm renders the E-mail address verification form.
|
|
* Parameters:
|
|
* ctxt - The AmContext for the request.
|
|
* Returns:
|
|
* Command string dictating what to be rendered.
|
|
* Data as a parameter for the command string.
|
|
* Standard Go error status.
|
|
*/
|
|
func VerifyEmailForm(ctxt ui.AmContext) (string, any, error) {
|
|
// Get target URI.
|
|
target := ctxt.Parameter("tgt")
|
|
if target == "" {
|
|
target = "/"
|
|
}
|
|
|
|
// If user is not logged in, this is an error.
|
|
user := ctxt.CurrentUser()
|
|
if !user.IsAnon {
|
|
return ui.ErrorPage(ctxt, errors.New("you must log in before you can verify your account's E-mail address"))
|
|
}
|
|
|
|
// If user is already verified, this is a no-op.
|
|
if user.VerifyEMail {
|
|
return "redirect", target, nil
|
|
}
|
|
|
|
dlg, err := ui.AmLoadDialog("verify_email")
|
|
if err == nil {
|
|
dlg.Field("tgt").Value = target
|
|
return dlg.Render(ctxt)
|
|
}
|
|
return ui.ErrorPage(ctxt, err)
|
|
}
|
|
|
|
func sendEmailConfirmationEmail(user *database.User, ci *database.ContactInfo, remoteIP string) error {
|
|
if ci != nil && ci.Email != nil && *ci.Email != "" {
|
|
msg := email.AmNewEmailMessage(user.Uid, remoteIP)
|
|
msg.AddTo(*ci.Email, "")
|
|
msg.SetTemplate("verify_email.jet")
|
|
msg.AddVariable("username", user.Username)
|
|
msg.AddVariable("confnum", user.EmailConfNum)
|
|
msg.Send()
|
|
return nil
|
|
} else {
|
|
return errors.New("cannot find email address")
|
|
}
|
|
}
|
|
|
|
/* VerifyEmail handles E-mail address verification.
|
|
* Parameters:
|
|
* ctxt - The AmContext for the request.
|
|
* Returns:
|
|
* Command string dictating what to be rendered.
|
|
* Data as a parameter for the command string.
|
|
* Standard Go error status.
|
|
*/
|
|
func VerifyEMail(ctxt ui.AmContext) (string, any, error) {
|
|
// If user is not logged in, this is an error.
|
|
user := ctxt.CurrentUser()
|
|
if !user.IsAnon {
|
|
return ui.ErrorPage(ctxt, errors.New("you must log in before you can verify your account's E-mail address"))
|
|
}
|
|
|
|
dlg, err := ui.AmLoadDialog("verify_email")
|
|
if err == nil {
|
|
dlg.LoadFromForm(ctxt)
|
|
target := dlg.Field("tgt").Value
|
|
if target == "" {
|
|
target = "/"
|
|
}
|
|
|
|
// If user is already verified, this is a no-op.
|
|
if user.VerifyEMail {
|
|
return "redirect", target, nil
|
|
}
|
|
|
|
action := dlg.WhichButton(ctxt)
|
|
if action == "cancel" { // Cancel button pressed
|
|
return "redirect", target, nil
|
|
}
|
|
if action == "sendagain" {
|
|
var ci *database.ContactInfo
|
|
ci, err = user.ContactInfo()
|
|
if err == nil {
|
|
err = user.NewEmailConfirmationNumber()
|
|
if err == nil {
|
|
err = sendEmailConfirmationEmail(user, ci, ctxt.RemoteIP())
|
|
}
|
|
}
|
|
if err == nil {
|
|
return dlg.RenderInfo(ctxt, "Verification message has been sent to your E-mail address.")
|
|
} else {
|
|
return dlg.RenderError(ctxt, err.Error())
|
|
}
|
|
}
|
|
if action == "ok" {
|
|
err = dlg.Validate()
|
|
if err == nil {
|
|
err = user.ConfirmEMailAddress(dlg.Field("num").AuxData.(int32), ctxt.RemoteIP())
|
|
if err == nil {
|
|
return "redirect", target, nil
|
|
}
|
|
}
|
|
return dlg.RenderError(ctxt, err.Error())
|
|
}
|
|
return dlg.RenderError(ctxt, "No known button click on POST to verify function.")
|
|
}
|
|
return ui.ErrorPage(ctxt, err)
|
|
}
|
|
|
|
/* NewAccountUserAgreement renders the Amsterdam user agreement for new accounts.
|
|
* Parameters:
|
|
* ctxt - The AmContext for the request.
|
|
* Returns:
|
|
* Command string dictating what to be rendered.
|
|
* Data as a parameter for the command string.
|
|
* Standard Go error status.
|
|
*/
|
|
func NewAccountUserAgreement(ctxt ui.AmContext) (string, any, error) {
|
|
// Get target URI.
|
|
target := ctxt.Parameter("tgt")
|
|
if target == "" {
|
|
target = "/"
|
|
}
|
|
|
|
// If user is already logged in, this is an error.
|
|
if !ctxt.CurrentUser().IsAnon {
|
|
return ui.ErrorPage(ctxt, errors.New("you cannot create a new account while logged in on an existing one. You must log out first"))
|
|
}
|
|
|
|
ctxt.VarMap().Set("target", target)
|
|
ctxt.VarMap().Set("amsterdam_pageTitle", "New Account User Agreement")
|
|
return "framed_template", "agreement.jet", nil
|
|
}
|
|
|
|
/* NewAccountUserAgreement renders the Amsterdam account creation form.
|
|
* Parameters:
|
|
* ctxt - The AmContext for the request.
|
|
* Returns:
|
|
* Command string dictating what to be rendered.
|
|
* Data as a parameter for the command string.
|
|
* Standard Go error status.
|
|
*/
|
|
func NewAccountForm(ctxt ui.AmContext) (string, any, error) {
|
|
// Get target URI.
|
|
target := ctxt.Parameter("tgt")
|
|
if target == "" {
|
|
target = "/"
|
|
}
|
|
|
|
// If user is already logged in, this is an error.
|
|
if !ctxt.CurrentUser().IsAnon {
|
|
return ui.ErrorPage(ctxt, fmt.Errorf("you cannot create a new account while logged in on an existing one. You must log out first"))
|
|
}
|
|
|
|
dlg, err := ui.AmLoadDialog("newaccount")
|
|
if err == nil {
|
|
dlg.Field("tgt").Value = target
|
|
dlg.Field("country").Value = "XX"
|
|
return dlg.Render(ctxt)
|
|
}
|
|
return ui.ErrorPage(ctxt, err)
|
|
}
|
|
|
|
/* NewAccount handles creating a new Amsterdam account.
|
|
* Parameters:
|
|
* ctxt - The AmContext for the request.
|
|
* Returns:
|
|
* Command string dictating what to be rendered.
|
|
* Data as a parameter for the command string.
|
|
* Standard Go error status.
|
|
*/
|
|
func NewAccount(ctxt ui.AmContext) (string, any, error) {
|
|
// If user is already logged in, this is an error.
|
|
if !ctxt.CurrentUser().IsAnon {
|
|
return ui.ErrorPage(ctxt, fmt.Errorf("you cannot create a new account while logged in on an existing one. You must log out first"))
|
|
}
|
|
|
|
dlg, err := ui.AmLoadDialog("newaccount")
|
|
if err == nil {
|
|
dlg.LoadFromForm(ctxt)
|
|
target := dlg.Field("tgt").Value
|
|
if target == "" {
|
|
target = "/"
|
|
}
|
|
|
|
action := dlg.WhichButton(ctxt)
|
|
if action == "cancel" { // Cancel button pressed
|
|
return "redirect", target, nil
|
|
}
|
|
if action == "create" {
|
|
err = dlg.Validate()
|
|
if err == nil {
|
|
if dlg.Field("pass1").Value != dlg.Field("pass2").Value {
|
|
return dlg.RenderError(ctxt, "The typed passwords do not match.")
|
|
}
|
|
var banned bool
|
|
banned, err = database.AmIsEmailAddressBanned(dlg.Field("email").Value)
|
|
if err == nil {
|
|
if banned {
|
|
return dlg.RenderError(ctxt, "This E-mail address may not register a new account.")
|
|
}
|
|
// Create new user account
|
|
var user *database.User
|
|
user, err = database.AmCreateNewUser(dlg.Field("user").Value, dlg.Field("pass1").Value,
|
|
dlg.Field("remind").Value, dlg.Field("dob").AsDate(), ctxt.RemoteIP())
|
|
if err == nil {
|
|
// create and save contact info
|
|
ci := database.AmNewUserContactInfo(user.Uid)
|
|
ci.Prefix = dlg.Field("prefix").ValPtr()
|
|
ci.GivenName = dlg.Field("first").Value
|
|
ci.MiddleInit = dlg.Field("mid").Value
|
|
if ci.MiddleInit == "" {
|
|
ci.MiddleInit = " "
|
|
}
|
|
ci.FamilyName = dlg.Field("last").Value
|
|
ci.Suffix = dlg.Field("suffix").ValPtr()
|
|
ci.Locality = dlg.Field("loc").ValPtr()
|
|
ci.Region = dlg.Field("reg").ValPtr()
|
|
ci.PostalCode = dlg.Field("pcode").ValPtr()
|
|
ci.Country = dlg.Field("country").ValPtr()
|
|
ci.Email = dlg.Field("email").ValPtr()
|
|
_, err = ci.Save()
|
|
if err == nil {
|
|
err = sendEmailConfirmationEmail(user, ci, ctxt.RemoteIP())
|
|
}
|
|
if err == nil {
|
|
// user is now logged in! redirect to E-mail verification
|
|
ctxt.ReplaceUser(user)
|
|
return "redirect", "/verify?tgt=" + url.PathEscape(target), nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return dlg.RenderError(ctxt, err.Error())
|
|
}
|
|
return dlg.RenderError(ctxt, "No known button click on POST to new account.")
|
|
}
|
|
return ui.ErrorPage(ctxt, err)
|
|
}
|
|
|
|
func PasswordRecovery(ctxt ui.AmContext) (string, any, error) {
|
|
var emailaddy string
|
|
uid, err := ctxt.URLParamInt("uid")
|
|
if err == nil {
|
|
auth, err := ctxt.URLParamInt("auth")
|
|
if err == nil {
|
|
pchange := database.AmGetPasswordChangeRequest(int32(uid))
|
|
if pchange == nil {
|
|
return ui.ErrorPage(ctxt, errors.New("password change request not found"))
|
|
}
|
|
if auth != int(pchange.Authentication) {
|
|
return ui.ErrorPage(ctxt, errors.New("invalid password change request"))
|
|
}
|
|
if time.Now().Compare(pchange.Expires) > 0 {
|
|
return ui.ErrorPage(ctxt, errors.New("password change request has expired"))
|
|
}
|
|
emailaddy = pchange.Email
|
|
}
|
|
}
|
|
|
|
if err == nil {
|
|
user, err := database.AmGetUser(int32(uid))
|
|
if err == nil {
|
|
newpass := util.GenerateRandomPassword()
|
|
err = user.ChangePassword(newpass, ctxt.RemoteIP())
|
|
if err == nil {
|
|
// send the password change message
|
|
msg := email.AmNewEmailMessage(user.Uid, ctxt.RemoteIP())
|
|
msg.AddTo(emailaddy, "")
|
|
msg.SetTemplate("pass_change.jet")
|
|
msg.AddVariable("username", user.Username)
|
|
msg.AddVariable("password", newpass)
|
|
msg.Send()
|
|
ctxt.VarMap().Set("amsterdam_pageTitle", "Your Password Has Been Changed")
|
|
return "framed_template", "password_changed.jet", nil
|
|
}
|
|
}
|
|
}
|
|
return ui.ErrorPage(ctxt, err)
|
|
}
|