got most of the login functionality together

This commit is contained in:
2025-09-27 23:07:16 -06:00
parent 03f1d9f717
commit 9427535eb5
8 changed files with 283 additions and 5 deletions
+96
View File
@@ -0,0 +1,96 @@
/*
* 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/.
*/
// The database package contains database management and storage logic.
package database
import (
"fmt"
"time"
)
// AuditRecord holds an audit record instance.
type AuditRecord struct {
Record int64 `db:"record"`
OnDate time.Time `db:"on_date"`
Event int32 `db:"event"`
Uid int32 `db:"uid"`
CommId int32 `db:"commid"`
IP *string `db:"ip"`
Data1 *string `db:"data1"`
Data2 *string `db:"data2"`
Data3 *string `db:"data3"`
Data4 *string `db:"data4"`
}
// These are the audit record types.
const (
AuditPublishToFrontPage = 1
AuditLoginOK = 101
AuditLoginFail = 102
AuditAccountCreated = 103
AuditVerifyEmailOK = 104
AuditVerifyEmailFail = 105
AuditSetUserContactInfo = 106
AuditResendEmailConfirm = 107
AuditChangePassword = 108
AuditAdminSetUserContectInfo = 109
AuditAdminChangeUserPassword = 110
AuditAdminChangeUserAccount = 111
AuditAdminSetAccountSecurity = 112
AuditAdminLockUnlockAccount = 113
)
/* AmNewAudit creates a new audit record.
* Parameters:
* rectype - Audit record type.
* uid - User ID of the user.
* ip - User's IP address.
* data - Argument data values for the audit record.
* Returns:
* The audit record pointer.
*/
func AmNewAudit(rectype int32, uid int32, ip string, data ...string) *AuditRecord {
rc := AuditRecord{Event: rectype, Uid: uid, CommId: 0}
if len(ip) > 0 {
rc.IP = &ip
}
if data != nil {
l := len(data)
if l > 0 {
rc.Data1 = &(data[0])
}
if l > 1 {
rc.Data2 = &(data[1])
}
if l > 2 {
rc.Data3 = &(data[2])
}
if l > 3 {
rc.Data4 = &(data[3])
}
}
return &rc
}
// Store stores the audit record in the database.
func (ar *AuditRecord) Store() error {
if ar.Record > 0 {
return fmt.Errorf("audit record %d already stored", ar.Record)
}
moment := time.Now()
rs, err := amdb.Exec(`INSERT INTO audit (on_date, event, uid, commid, ip, data1, data2, data3, data4)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`, moment, ar.Event, ar.Uid, ar.CommId, ar.IP,
ar.Data1, ar.Data2, ar.Data3, ar.Data4)
if err != nil {
return err
}
ar.Record, _ = rs.LastInsertId()
ar.OnDate = moment
return nil
}
+96
View File
@@ -10,11 +10,15 @@
package database
import (
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"sync"
"time"
lru "github.com/hashicorp/golang-lru"
log "github.com/sirupsen/logrus"
)
// User represents a user in the Amsterdam database.
@@ -83,6 +87,32 @@ func AmGetUser(uid int32) (*User, error) {
return rc.(*User), err
}
/* AmGetUserByName returns a reference to the specified user.
* Parameters:
* name - The username of the user.
* Returns:
* Pointer to User containing user data, or nil
* Standard Go error status
*/
func AmGetUserByName(name string) (*User, error) {
var dbdata []User
err := amdb.Select(&dbdata, "SELECT * FROM users WHERE username = ?", name)
if err != nil {
return nil, err
}
if len(dbdata) > 1 {
return nil, fmt.Errorf("AmGetUserByName(\"%s\"): too many responses(%d)", name, len(dbdata))
}
getUserMutex.Lock()
defer getUserMutex.Unlock()
rc, ok := userCache.Get(dbdata[0].Uid)
if !ok {
rc = &(dbdata[0])
userCache.Add(dbdata[0].Uid, rc)
}
return rc.(*User), nil
}
// getAnonUserID retrieves the UID of the "anonymous" user from the database.
func getAnonUserID() (int32, error) {
if anonUid < 0 {
@@ -130,3 +160,69 @@ func AmGetAnonUser() (*User, error) {
}
return rc, err
}
// hashPassword hashes the password value.
func hashPassword(password string) string {
if len(password) == 0 {
return ""
}
hasher := sha1.New()
hasher.Write([]byte(password))
hashBytes := hasher.Sum(nil)
return hex.EncodeToString(hashBytes)
}
// touchUser updates the last access time for the user.
func touchUser(user *User) {
user.Mutex.Lock()
defer user.Mutex.Unlock()
moment := time.Now()
_, _ = amdb.Exec("UPDATE user SET lastaccess = ? WHERE uid = ?", moment, user.Uid)
user.LastAccess = &moment
}
/* AmAuthenticateUser authenticates a user by name and password.
* Parameters:
* name - The user name to try.
* password - The password to try.
* remote_ip - The remote IP address, for audit records.
* Returns:
* The User pointer if authenticated, or nil if not.
* Standard Go error status.
*/
func AmAuthenticateUser(name string, password string, remote_ip string) (*User, error) {
log.Debugf("AmAuthenicate() authenticating user %s...", name)
var ar *AuditRecord = nil
defer func() {
if ar != nil {
go ar.Store()
}
}()
user, err := AmGetUserByName(name)
if err != nil {
log.Error("...user not found")
ar = AmNewAudit(AuditLoginFail, 0, remote_ip, fmt.Sprintf("Bad username: %s", name))
return nil, errors.New("the user account you have specified does not exist; please try again")
}
if user.IsAnon {
log.Error("...user is the Anonymous Honyak, can't explicitly log in")
ar = AmNewAudit(AuditLoginFail, user.Uid, remote_ip, "Anonymous user")
return nil, errors.New("this account cannot be explicitly logged into; please try again")
}
if user.Lockout {
log.Error("...user is locked out by the admin")
ar = AmNewAudit(AuditLoginFail, user.Uid, remote_ip, "Account locked out")
return nil, errors.New("this account has been administratively locked; please contact the system administrator for assistance")
}
h := hashPassword(password)
if h != user.Passhash {
log.Warn("...invalid password")
ar = AmNewAudit(AuditLoginFail, user.Uid, remote_ip, "Bad password")
return nil, errors.New("the password you have specified is incorrect; please try again")
}
log.Debug("...authenticated")
touchUser(user)
ar = AmNewAudit(AuditLoginOK, user.Uid, remote_ip)
return user, nil
}