profile.gno
4.11 Kb · 170 lines
1package profile
2
3import (
4 "chain"
5
6 "gno.land/p/nt/avl/v0"
7 "gno.land/p/nt/mux/v0"
8 "gno.land/p/nt/ufmt/v0"
9)
10
11var (
12 fields = avl.NewTree()
13 router = mux.NewRouter()
14)
15
16// Standard fields
17const (
18 DisplayName = "DisplayName"
19 Homepage = "Homepage"
20 Bio = "Bio"
21 Age = "Age"
22 Location = "Location"
23 Avatar = "Avatar"
24 GravatarEmail = "GravatarEmail"
25 AvailableForHiring = "AvailableForHiring"
26 InvalidField = "InvalidField"
27)
28
29// Events
30const (
31 ProfileFieldCreated = "ProfileFieldCreated"
32 ProfileFieldUpdated = "ProfileFieldUpdated"
33)
34
35// Field types used when emitting event
36const FieldType = "FieldType"
37
38const (
39 BoolField = "BoolField"
40 StringField = "StringField"
41 IntField = "IntField"
42)
43
44func init() {
45 router.HandleFunc("", homeHandler)
46 router.HandleFunc("u/{addr}", profileHandler)
47 router.HandleFunc("f/{addr}/{field}", fieldHandler)
48}
49
50// List of supported string fields
51var stringFields = map[string]bool{
52 DisplayName: true,
53 Homepage: true,
54 Bio: true,
55 Location: true,
56 Avatar: true,
57 GravatarEmail: true,
58}
59
60// List of support int fields
61var intFields = map[string]bool{
62 Age: true,
63}
64
65// List of support bool fields
66var boolFields = map[string]bool{
67 AvailableForHiring: true,
68}
69
70// Setters
71//
72// Security properties (AAA-1 B5 — guard the vendored setters):
73//
74// 1. Address safety (caller-keyed): every key is `PreviousRealm().Address() + ":" + field`,
75// i.e. the immediate caller's own address — the address is NEVER a parameter.
76// So a caller (user OR another realm) can only ever write its OWN profile;
77// cross-address forgery is structurally impossible. This is why the upstream
78// demo is safe to vendor: a DAO calling these writes the DAO's profile, not a
79// member's, and no realm can impersonate another address.
80//
81// 2. Field allowlist (now ENFORCED): the stringFields/intFields/boolFields maps
82// existed but were never checked, so any caller could write arbitrary field
83// names into the shared tree (state-bloat / unexpected-field injection on its
84// own profile). Each setter now rejects unknown fields. basedao only ever sets
85// DisplayName/Bio/Avatar (all allowlisted), so this does not break the DAO path.
86
87func SetStringField(cur realm, field, value string) bool {
88 if !stringFields[field] {
89 panic("unknown string profile field: " + field)
90 }
91 addr := cur.Previous().Address()
92 key := addr.String() + ":" + field
93 updated := fields.Set(key, value)
94
95 event := ProfileFieldCreated
96 if updated {
97 event = ProfileFieldUpdated
98 }
99
100 chain.Emit(event, FieldType, StringField, field, value)
101
102 return updated
103}
104
105func SetIntField(cur realm, field string, value int) bool {
106 if !intFields[field] {
107 panic("unknown int profile field: " + field)
108 }
109 addr := cur.Previous().Address()
110 key := addr.String() + ":" + field
111 updated := fields.Set(key, value)
112
113 event := ProfileFieldCreated
114 if updated {
115 event = ProfileFieldUpdated
116 }
117
118 // ufmt.Sprintf("%d", …): string(int) would coerce the value to a single rune
119 // (e.g. 65 → "A"), corrupting the event payload.
120 chain.Emit(event, FieldType, IntField, field, ufmt.Sprintf("%d", value))
121
122 return updated
123}
124
125func SetBoolField(cur realm, field string, value bool) bool {
126 if !boolFields[field] {
127 panic("unknown bool profile field: " + field)
128 }
129 addr := cur.Previous().Address()
130 key := addr.String() + ":" + field
131 updated := fields.Set(key, value)
132
133 event := ProfileFieldCreated
134 if updated {
135 event = ProfileFieldUpdated
136 }
137
138 chain.Emit(event, FieldType, BoolField, field, ufmt.Sprintf("%t", value))
139
140 return updated
141}
142
143// Getters
144
145func GetStringField(addr address, field, def string) string {
146 key := addr.String() + ":" + field
147 if value, ok := fields.Get(key); ok {
148 return value.(string)
149 }
150
151 return def
152}
153
154func GetBoolField(addr address, field string, def bool) bool {
155 key := addr.String() + ":" + field
156 if value, ok := fields.Get(key); ok {
157 return value.(bool)
158 }
159
160 return def
161}
162
163func GetIntField(addr address, field string, def int) int {
164 key := addr.String() + ":" + field
165 if value, ok := fields.Get(key); ok {
166 return value.(int)
167 }
168
169 return def
170}