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}