Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

user.gno

5.03 Kb · 216 lines
  1package user
  2
  3import (
  4	"chain"
  5	"net/url"
  6	"strconv"
  7	"time"
  8
  9	"gno.land/p/akkadia/beta/accesscontrol"
 10	"gno.land/p/nt/bptree/v0"
 11	"gno.land/r/akkadia/beta/admin"
 12)
 13
 14const (
 15	CreateUserEvent = "CreateUser"
 16	UpdateUserEvent = "UpdateUser"
 17	DeleteUserEvent = "DeleteUser"
 18
 19	createdAtKey = "created_at"
 20)
 21
 22// users is address string -> map[string]string.
 23// The zero value is an empty BPTree with fanout 32.
 24var users bptree.BPTree
 25
 26func HasUser(addr address) bool {
 27	assertIsValidAddress(addr)
 28	return users.Has(addr.String())
 29}
 30
 31func CreateUser(cur realm) {
 32	addr := accesscontrol.MustGetUserCaller(0, cur)
 33	if isFrozen {
 34		panic("user self-service is frozen")
 35	}
 36	addrStr := addr.String()
 37	if users.Has(addrStr) {
 38		panic("user already exists: " + addrStr)
 39	}
 40	users.Set(addrStr, map[string]string{
 41		createdAtKey: strconv.FormatInt(time.Now().Unix(), 10),
 42	})
 43	chain.Emit(CreateUserEvent, "address", addrStr)
 44}
 45
 46// AdminCreateUser is idempotent so an operator can safely retry registration.
 47func AdminCreateUser(cur realm, addr address) {
 48	accesscontrol.AssertIsAdminOrOperator(0, cur, admin.IsAdmin, admin.IsOperator)
 49	assertIsValidAddress(addr)
 50	addrStr := addr.String()
 51	if users.Has(addrStr) {
 52		return
 53	}
 54	users.Set(addrStr, map[string]string{
 55		createdAtKey: strconv.FormatInt(time.Now().Unix(), 10),
 56	})
 57	chain.Emit(CreateUserEvent, "address", addrStr)
 58}
 59
 60func DeleteUser(cur realm) {
 61	addr := accesscontrol.MustGetUserCaller(0, cur)
 62	if isFrozen {
 63		panic("user self-service is frozen")
 64	}
 65	deleteUser(addr)
 66}
 67
 68func AdminDeleteUser(cur realm, addr address) {
 69	accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin)
 70	assertIsValidAddress(addr)
 71	deleteUser(addr)
 72}
 73
 74// DeleteAll removes registrations while preserving freeze and list settings.
 75func DeleteAll(cur realm) {
 76	accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin)
 77	users = bptree.BPTree{}
 78}
 79
 80func AdminUpdateUser(cur realm, addr address, propKeys, propValues string) {
 81	accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin)
 82	assertIsValidAddress(addr)
 83	addrStr := addr.String()
 84	current := users.Get(addrStr)
 85	if current == nil {
 86		panic("user not found: " + addrStr)
 87	}
 88	props := copyStringMap(current.(map[string]string))
 89	updates := parsePropertiesCSV(propKeys, propValues)
 90	for key, value := range updates {
 91		props[key] = value
 92	}
 93	users.Set(addrStr, props)
 94	chain.Emit(UpdateUserEvent, "address", addrStr)
 95}
 96
 97func GetTotalUserSize() int {
 98	return users.Size()
 99}
100
101func ListUsers(page, count int) []string {
102	assertListPageCount(page, count)
103	result := []string{}
104	offset := (page - 1) * count
105	users.IterateByOffset(offset, count, func(key string, _ any) bool {
106		result = append(result, key)
107		return false
108	})
109	return result
110}
111
112func ListUsersByAddresses(addrs ...address) []map[string]string {
113	assertListLimit("addrs", len(addrs))
114	result := []map[string]string{}
115	for _, addr := range addrs {
116		assertIsValidAddress(addr)
117		addrStr := addr.String()
118		current := users.Get(addrStr)
119		if current == nil {
120			continue
121		}
122		props := copyStringMap(current.(map[string]string))
123		props["address"] = addrStr
124		result = append(result, props)
125	}
126	return result
127}
128
129func deleteUser(addr address) {
130	addrStr := addr.String()
131	if _, removed := users.Remove(addrStr); !removed {
132		panic("user not found: " + addrStr)
133	}
134	chain.Emit(DeleteUserEvent, "address", addrStr)
135}
136
137func copyStringMap(source map[string]string) map[string]string {
138	result := map[string]string{}
139	for key, value := range source {
140		result[key] = value
141	}
142	return result
143}
144
145// parsePropertiesCSV accepts comma-separated keys and comma-separated values.
146// Each value must be URL-encoded so commas and other separators can be carried
147// safely; this parser URL-decodes values before returning the map.
148func parsePropertiesCSV(keys, values string) map[string]string {
149	if keys == "" {
150		panic("properties keys must be not empty")
151	}
152
153	result := map[string]string{}
154	keyStart, valStart := 0, 0
155	keyIdx, valIdx := 0, 0
156	for {
157		for keyIdx < len(keys) && keys[keyIdx] != ',' {
158			keyIdx++
159		}
160		for valIdx < len(values) && values[valIdx] != ',' {
161			valIdx++
162		}
163
164		key := keys[keyStart:keyIdx]
165		if key == "" {
166			panic("properties key must be not empty")
167		}
168		if _, found := result[key]; found {
169			panic("properties key duplicated: " + key)
170		}
171
172		value, err := url.PathUnescape(values[valStart:valIdx])
173		if err != nil {
174			panic("properties value URL decode failed: " + key)
175		}
176		result[key] = value
177
178		keyEnd := keyIdx >= len(keys)
179		valEnd := valIdx >= len(values)
180		if keyEnd != valEnd {
181			panic("properties keys and values count mismatch")
182		}
183		if keyEnd {
184			break
185		}
186
187		keyIdx++
188		valIdx++
189		keyStart = keyIdx
190		valStart = valIdx
191	}
192
193	return result
194}
195
196func assertIsValidAddress(addr address) {
197	if !addr.IsValid() {
198		panic("invalid address: " + addr.String())
199	}
200}
201
202func assertListLimit(field string, count int) {
203	if count > listLimit {
204		panic(field + " exceeds listLimit (max: " + strconv.Itoa(listLimit) + ")")
205	}
206}
207
208func assertListPageCount(page int, count int) {
209	if page < 1 {
210		panic("page must be at least 1")
211	}
212	if count < 1 {
213		panic("count must be at least 1")
214	}
215	assertListLimit("count", count)
216}