working on Import User Accounts (unfinished)

This commit is contained in:
2026-02-26 23:14:06 -07:00
parent 9dc4dd2ec4
commit a04b484e41
6 changed files with 323 additions and 3 deletions
+135
View File
@@ -13,6 +13,7 @@ import (
"context"
"encoding/xml"
"errors"
"time"
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/util"
@@ -280,3 +281,137 @@ func VCardFromContactInfo(ctx context.Context, target *VCard, ci *database.Conta
}
return nil
}
// VCardSetContactINfo fills the ContactInfo object with data from the VCard.
func VCardSetContactInfo(ci *database.ContactInfo, vc *VCard) {
ci.GivenName = util.IIF(vc.Name.Given == "", nil, &vc.Name.Given)
ci.FamilyName = util.IIF(vc.Name.Family == "", nil, &vc.Name.Family)
if vc.Name.Middle == "" {
ci.MiddleInit = nil
} else {
s := vc.Name.Middle[0:1]
ci.MiddleInit = &s
}
ci.Prefix = util.IIF(vc.Name.Prefix == "", nil, &vc.Name.Prefix)
ci.Suffix = util.IIF(vc.Name.Suffix == "", nil, &vc.Name.Suffix)
if vc.Org != nil {
ci.Company = &(vc.Org.OrgName)
}
if vc.URL != "" {
ci.URL = &(vc.URL)
}
addr := VCardSelectAddress(vc)
if addr != nil {
ci.Addr1 = util.IIF(addr.Street == "", nil, &addr.Street)
ci.Addr2 = util.IIF(addr.ExtAddr == "", nil, &addr.ExtAddr)
ci.Locality = util.IIF(addr.Locality == "", nil, &addr.Locality)
ci.Region = util.IIF(addr.Region == "", nil, &addr.Region)
ci.PostalCode = util.IIF(addr.PCode == "", nil, &addr.PCode)
ci.Country = util.IIF(addr.Country == "", nil, &addr.Country)
}
email, err := VCardGetEmailAddress(vc)
if err == nil {
ci.Email = &email
}
phone, fax, mobile := VCardSelectPhones(vc)
if phone != nil {
ci.Phone = &(phone.Number)
}
if fax != nil {
ci.Fax = &(fax.Number)
}
if mobile != nil {
ci.Mobile = &(mobile.Number)
}
}
// VCardSelectAddress selects a valid address from the VCard.
func VCardSelectAddress(vc *VCard) *VCAddress {
if vc.Address == nil || len(*vc.Address) == 0 {
return nil
}
if len(*vc.Address) == 1 {
return &((*vc.Address)[0])
}
for i := range *vc.Address {
if (*vc.Address)[i].Preferred != nil {
return &((*vc.Address)[i])
}
}
return &((*vc.Address)[0])
}
// VCardSelectPhones finds the phone, fax, and mobile numbers in the telephone block.
func VCardSelectPhones(vc *VCard) (*VCTelephone, *VCTelephone, *VCTelephone) {
if vc.Tel == nil || len(*vc.Tel) == 0 {
return nil, nil, nil
}
var mobile *VCTelephone = nil
for i := range *vc.Tel {
if (*vc.Tel)[i].Cell != nil {
if mobile == nil || (*vc.Tel)[i].Preferred != nil {
mobile = &((*vc.Tel)[i])
}
}
}
var fax *VCTelephone = nil
for i := range *vc.Tel {
if (*vc.Tel)[i].Fax != nil {
if fax == nil || (*vc.Tel)[i].Preferred != nil {
fax = &((*vc.Tel)[i])
}
}
}
var phone *VCTelephone = nil
for i := range *vc.Tel {
if (*vc.Tel)[i].Voice != nil && (*vc.Tel)[i].Cell == nil {
if phone == nil || (*vc.Tel)[i].Preferred != nil {
phone = &((*vc.Tel)[i])
}
}
}
return phone, fax, mobile
}
// VCardGetEmailAddress finds a useful E-mail address in a VCard.
func VCardGetEmailAddress(vc *VCard) (string, error) {
if vc.Email == nil || len(*vc.Email) == 0 {
return "", errors.New("no E-mail address found for user")
}
addrs := make([]*VCEmail, 0, len(*vc.Email))
for i, a := range *vc.Email {
if a.Internet != nil {
addrs = append(addrs, &((*vc.Email)[i]))
}
}
if len(addrs) == 0 {
return "", errors.New("no Internet E-mail addresses found for user")
}
if len(addrs) == 1 {
return addrs[0].UserID, nil
}
for _, a := range addrs {
if a.Preferred != nil {
return a.UserID, nil
}
}
for _, a := range addrs {
if a.Home != nil {
return a.UserID, nil
}
}
return addrs[0].UserID, nil
}
// VCardGetBirthday extracts the birthday from the VCard as a time value.
func VCardGetBirthday(vc *VCard) (*time.Time, error) {
s := vc.BDay
if s == "" {
return nil, nil
}
if len(s) > 8 {
s = s[:8]
}
val, err := time.Parse(ISO8601_DATE, s)
return &val, err
}
+72 -1
View File
@@ -18,6 +18,7 @@ import (
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/util"
log "github.com/sirupsen/logrus"
)
/*
@@ -186,10 +187,80 @@ func VIUStreamCommunityMemberList(ctx context.Context, w io.Writer, comm *databa
if err != nil {
return fmt.Errorf("error converting user %d: %v", u.Uid, err)
}
enc.Encode(encodedUser)
err = enc.Encode(encodedUser)
if err != nil {
log.Warnf("error dumping XML for user %d: %v", u.Uid, err)
}
}
// Write the trailing tag.
_, err = w.Write([]byte("</venice-import-users>\r\n"))
return err
}
func VIUCreateUser(ctx context.Context, udata *VIUUser, loader *database.User, ipaddr string) error {
if !database.AmIsValidAmsterdamID(udata.Username) {
return fmt.Errorf("the username \"%s\" is not a valid Amsterdam ID")
}
email, err := VCardGetEmailAddress(&(udata.VCard))
if err != nil {
return err
}
ban, err := database.AmIsEmailAddressBanned(ctx, email)
if err != nil {
return err
} else if ban {
return fmt.Errorf("the E-mail address %s has been banned", email)
}
dob, err := VCardGetBirthday(&(udata.VCard))
if err != nil {
return err
}
pwd := udata.Password.Hash
if udata.Password.Prehashed {
pwd = ""
}
user, err := database.AmCreateNewUser(ctx, udata.Username, pwd, udata.PasswordReminder, dob, ipaddr)
if err != nil {
return err
}
ci := database.AmNewUserContactInfo(user.Uid)
VCardSetContactInfo(ci, &(udata.VCard))
ci.PrivateAddr = udata.Options.HideAddr
ci.PrivatePhone = udata.Options.HidePhone
ci.PrivateFax = udata.Options.HideFax
ci.PrivateEmail = udata.Options.HideEmail
_, err = ci.Save(ctx, loader, ipaddr)
if err != nil {
return err
}
err = user.SetContactID(ctx, ci.ContactId)
if err != nil {
return err
}
// TODO
return nil
}
func VIUImportUserList(ctx context.Context, r io.Reader, loader *database.User, ipaddr string) (int, []string, error) {
dec := xml.NewDecoder(r)
var importData VIUBase
err := dec.Decode(&importData)
if err != nil {
return 0, make([]string, 0), err
}
scroll := make([]string, 0, len(importData.Users))
userCount := 0
for _, udata := range importData.Users {
err = VIUCreateUser(ctx, &udata, loader, ipaddr)
if err != nil {
scroll = append(scroll, fmt.Sprintf("Error creating user \"%s\": %v", udata.Username, err))
} else {
scroll = append(scroll, fmt.Sprintf("User \"%v\" created", udata.Username))
userCount++
}
}
return userCount, scroll, nil
}
+2 -1
View File
@@ -111,6 +111,7 @@ func setupEcho() *echo.Echo {
sysGroup.GET("/ipban/add", ui.AmWrap(AddIPBanForm))
sysGroup.POST("/ipban/add", ui.AmWrap(AddIPBan))
sysGroup.Match(GetAndPost, "/audit", ui.AmWrap(SystemAudit))
sysGroup.Match(GetAndPost, "/import", ui.AmWrap(UserImport))
// community group
uiset2 := make([]echo.MiddlewareFunc, len(uiset), len(uiset)+1)
@@ -247,7 +248,7 @@ func main() {
}()
stime := time.Since(start)
log.Infof("Amsterdam startup sequence completed in %v", stime)
log.Infof("Amsterdam %s startup sequence completed in %v", config.AMSTERDAM_VERSION, stime)
// Start server
go func() {
+49
View File
@@ -684,6 +684,13 @@ func AddIPBan(ctxt ui.AmContext) (string, any) {
return dlg.RenderError(ctxt, err.Error())
}
/* SystemAudit displays the system audit loga.
* Parameters:
* ctxt - The AmContext for the request.
* Returns:
* Command string dictating what to be rendered.
* Data as a parameter for the command string.
*/
func SystemAudit(ctxt ui.AmContext) (string, any) {
if !database.AmTestPermission("Global.SysAdminAccess", ctxt.CurrentUser().BaseLevel) {
return "error", ENOACCESS
@@ -748,3 +755,45 @@ func SystemAudit(ctxt ui.AmContext) (string, any) {
ctxt.SetFrameTitle("System Audit Records")
return "framed", "audit.jet"
}
/* UserImport handles importing user accounts.
* Parameters:
* ctxt - The AmContext for the request.
* Returns:
* Command string dictating what to be rendered.
* Data as a parameter for the command string.
*/
func UserImport(ctxt ui.AmContext) (string, any) {
if !database.AmTestPermission("Global.SysAdminAccess", ctxt.CurrentUser().BaseLevel) {
return "error", ENOACCESS
}
if ctxt.Verb() == "GET" {
ctxt.SetFrameTitle("Import User Accounts")
return "framed", "import_users.jet"
}
if ctxt.FormFieldIsSet("cancel") {
return "redirect", "/sysadmin"
} else if !ctxt.FormFieldIsSet("upload") {
return "error", EBUTTON
}
importData, err := ctxt.FormFile("idata")
if err != nil {
ctxt.VarMap().Set("errorMessage", err.Error())
ctxt.SetFrameTitle("Import User Accounts")
return "framed", "import_users.jet"
}
f, err := importData.Open()
if err != nil {
ctxt.VarMap().Set("errorMessage", err.Error())
ctxt.SetFrameTitle("Import User Accounts")
return "framed", "import_users.jet"
}
f.Close()
return "error", "Not yet implemented"
}
+1 -1
View File
@@ -50,7 +50,7 @@ menudefs:
link: "/sysadmin/audit"
permission: "Global.SysAdminAccess"
- text: "Import User Accounts"
link: "/TODO/sysadmin/import"
link: "/sysadmin/import"
permission: "Global.SysAdminAccess"
- id: "communityadmin"
title: "Community Administration:"
+64
View File
@@ -0,0 +1,64 @@
{*
* Amsterdam Web Communities System
* Copyright (c) 2025-2026 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/.
*}
<!-- Page Title -->
<div class="p-4">
<div class="mb-6">
<h1 class="text-blue-800 text-4xl font-bold mb-2">Import User Accounts</h1>
<hr class="border-2 border-gray-400 w-4/5 mb-4">
</div>
{{ if isset(errorMessage) }}
<!-- Error Message Banner -->
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6" id="error-banner">
<div class="flex items-center">
<div class="flex-shrink-0">
<span class="text-red-500 text-xl">⚠️</span>
</div>
<div class="ml-3">
<p class="text-sm font-medium" id="error-message">{{ CapitalizeString(errorMessage) }}.</p>
</div>
</div>
</div>
{{ end }}
<!-- Upload Form -->
<form method="POST" enctype="multipart/form-data" action="/sysadmin/import" class="max-w-2xl">
<div class="bg-gray-50 p-6 rounded-lg">
<div class="mb-6">
<label for="idata" class="block text-black text-sm font-medium mb-2">
User import data:
</label>
<input type="file" id="idata" name="idata"
class="block w-full text-sm text-gray-900 border border-gray-300 rounded cursor-pointer bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-blue-600 file:text-white hover:file:bg-blue-700">
</div>
<!-- Action Buttons -->
<div class="flex gap-4">
<button type="submit"
name="upload"
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">
Upload
</button>
<button type="submit"
name="cancel"
class="bg-red-600 hover:bg-red-700 text-white px-8 py-2 rounded font-medium transition-colors">
Cancel
</button>
</div>
<!-- Additional Information -->
<div class="mt-6 p-4 bg-blue-50 border border-blue-200 rounded">
<h3 class="text-sm font-bold text-blue-900 mb-2">User Upload Guidelines:</h3>
<ul class="text-xs text-blue-800 space-y-1 list-disc list-inside">
<li>The user accounts will be imported as a <code>venice-import-users</code> XML file.</li>
</ul>
</div>
</div>
</form>
</div>