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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}