working on Import User Accounts (unfinished)
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.erbosoft.com/amy/amsterdam/database"
|
"git.erbosoft.com/amy/amsterdam/database"
|
||||||
"git.erbosoft.com/amy/amsterdam/util"
|
"git.erbosoft.com/amy/amsterdam/util"
|
||||||
@@ -280,3 +281,137 @@ func VCardFromContactInfo(ctx context.Context, target *VCard, ci *database.Conta
|
|||||||
}
|
}
|
||||||
return nil
|
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/database"
|
||||||
"git.erbosoft.com/amy/amsterdam/util"
|
"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 {
|
if err != nil {
|
||||||
return fmt.Errorf("error converting user %d: %v", u.Uid, err)
|
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.
|
// Write the trailing tag.
|
||||||
_, err = w.Write([]byte("</venice-import-users>\r\n"))
|
_, err = w.Write([]byte("</venice-import-users>\r\n"))
|
||||||
return err
|
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.GET("/ipban/add", ui.AmWrap(AddIPBanForm))
|
||||||
sysGroup.POST("/ipban/add", ui.AmWrap(AddIPBan))
|
sysGroup.POST("/ipban/add", ui.AmWrap(AddIPBan))
|
||||||
sysGroup.Match(GetAndPost, "/audit", ui.AmWrap(SystemAudit))
|
sysGroup.Match(GetAndPost, "/audit", ui.AmWrap(SystemAudit))
|
||||||
|
sysGroup.Match(GetAndPost, "/import", ui.AmWrap(UserImport))
|
||||||
|
|
||||||
// community group
|
// community group
|
||||||
uiset2 := make([]echo.MiddlewareFunc, len(uiset), len(uiset)+1)
|
uiset2 := make([]echo.MiddlewareFunc, len(uiset), len(uiset)+1)
|
||||||
@@ -247,7 +248,7 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
stime := time.Since(start)
|
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
|
// Start server
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
+49
@@ -684,6 +684,13 @@ func AddIPBan(ctxt ui.AmContext) (string, any) {
|
|||||||
return dlg.RenderError(ctxt, err.Error())
|
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) {
|
func SystemAudit(ctxt ui.AmContext) (string, any) {
|
||||||
if !database.AmTestPermission("Global.SysAdminAccess", ctxt.CurrentUser().BaseLevel) {
|
if !database.AmTestPermission("Global.SysAdminAccess", ctxt.CurrentUser().BaseLevel) {
|
||||||
return "error", ENOACCESS
|
return "error", ENOACCESS
|
||||||
@@ -748,3 +755,45 @@ func SystemAudit(ctxt ui.AmContext) (string, any) {
|
|||||||
ctxt.SetFrameTitle("System Audit Records")
|
ctxt.SetFrameTitle("System Audit Records")
|
||||||
return "framed", "audit.jet"
|
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"
|
link: "/sysadmin/audit"
|
||||||
permission: "Global.SysAdminAccess"
|
permission: "Global.SysAdminAccess"
|
||||||
- text: "Import User Accounts"
|
- text: "Import User Accounts"
|
||||||
link: "/TODO/sysadmin/import"
|
link: "/sysadmin/import"
|
||||||
permission: "Global.SysAdminAccess"
|
permission: "Global.SysAdminAccess"
|
||||||
- id: "communityadmin"
|
- id: "communityadmin"
|
||||||
title: "Community Administration:"
|
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