working on Import User Accounts (unfinished)
This commit is contained in:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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:"
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user