1
1
< html >
2
- < head >
3
- < link href ="
https://cdn.jsdelivr.net/npm/[email protected] /dist/full.min.css "
rel ="
stylesheet "
type ="
text/css "
/>
4
- < script src ="https://cdn.tailwindcss.com?plugins=typography "> </ script >
5
- < script src ="https://unpkg.com/alpinejs " defer > </ script >
6
- < script >
7
- const API_BASE_URL = "%%API_BASE_URL%%" ;
8
- const hexes = Array . from ( { length : 256 } , ( _ , i ) => i . toString ( 16 ) . padStart ( 2 , '0' ) ) ;
9
-
10
- function bytesToHex ( bytes ) {
11
- let hex = '' ;
12
- for ( let i = 0 ; i < bytes . length ; i ++ ) {
13
- hex += hexes [ bytes [ i ] ] ;
14
- }
15
- return hex ;
2
+
3
+ < head >
4
+ < link href ="
https://cdn.jsdelivr.net/npm/[email protected] /dist/full.min.css "
rel ="
stylesheet "
type ="
text/css "
/>
5
+ < script src ="https://cdn.tailwindcss.com?plugins=typography "> </ script >
6
+ < script src ="https://unpkg.com/alpinejs " defer > </ script >
7
+ < script >
8
+ const ADMINISTERED_HOST = "%%ADMINISTERED_HOST%%" ;
9
+ const API_BASE_URL = "%%API_BASE_URL%%" ;
10
+
11
+ const hexes = Array . from ( { length : 256 } , ( _ , i ) => i . toString ( 16 ) . padStart ( 2 , '0' ) ) ;
12
+
13
+ function bytesToHex ( bytes ) {
14
+ let hex = '' ;
15
+ for ( let i = 0 ; i < bytes . length ; i ++ ) {
16
+ hex += hexes [ bytes [ i ] ] ;
16
17
}
18
+ return hex ;
19
+ }
20
+
21
+ async function getEvent ( kind , content , tags ) {
22
+ let event = { 'kind' : kind , 'content' : content , 'tags' : tags } ;
23
+ event . pubkey = await window . nostr . getPublicKey ( ) ;
24
+ event . created_at = Math . round ( new Date ( ) . getTime ( ) / 1000 ) ;
25
+ serialized = JSON . stringify ( [ 0 , event . pubkey , event . created_at , event . kind , event . tags , event . content ] ) ;
26
+ event . id = bytesToHex ( new Uint8Array ( await window . crypto . subtle . digest ( "SHA-256" , ( new TextEncoder ( ) ) . encode ( serialized ) ) ) ) ;
27
+ return await window . nostr . signEvent ( event ) ;
28
+ }
29
+
30
+ async function getNostrAuthHeader ( url , method ) {
31
+ let authEvent = await getEvent ( 27235 , "" , [ [ 'u' , url ] , [ 'method' , method ] ] ) ;
32
+ return `Nostr ${ btoa ( JSON . stringify ( authEvent ) ) } ` ;
33
+ }
17
34
18
- async function getEvent ( kind , content , tags ) {
19
- let event = { 'kind' : kind , 'content' : content , 'tags' : tags } ;
20
- event . pubkey = await window . nostr . getPublicKey ( ) ;
21
- event . created_at = Math . round ( new Date ( ) . getTime ( ) / 1000 ) ;
22
- serialized = JSON . stringify ( [ 0 , event . pubkey , event . created_at , event . kind , event . tags , event . content ] ) ;
23
- event . id = bytesToHex ( new Uint8Array ( await window . crypto . subtle . digest ( "SHA-256" , ( new TextEncoder ( ) ) . encode ( serialized ) ) ) ) ;
24
- return await window . nostr . signEvent ( event ) ;
35
+ async function getSites ( sites ) {
36
+ while ( ! window . nostr ) {
37
+ await new Promise ( r => setTimeout ( r , 0 ) ) ;
25
38
}
26
39
27
- async function getNostrAuthHeader ( url , method ) {
28
- let authEvent = await getEvent ( 27235 , "" , [ [ 'u' , url ] , [ 'method' , method ] ] ) ;
29
- return `Nostr ${ btoa ( JSON . stringify ( authEvent ) ) } ` ;
40
+ let endpoint = `${ API_BASE_URL } /api/sites` ;
41
+ sites . length = 0 ;
42
+ let response = await fetch ( endpoint , {
43
+ headers : { 'Authorization' : await getNostrAuthHeader ( window . location . protocol + endpoint , 'GET' ) } ,
44
+ } ) ;
45
+ for ( const s of await response . json ( ) ) {
46
+ sites . push ( s ) ;
30
47
}
48
+ }
31
49
32
- async function getSites ( sites ) {
33
- while ( ! window . nostr ) {
34
- await new Promise ( r => setTimeout ( r , 0 ) ) ;
35
- }
36
- let endpoint = `${ API_BASE_URL } /api/sites` ;
37
- sites . length = 0 ;
38
- let response = await fetch ( endpoint , {
39
- headers : { 'Authorization' : await getNostrAuthHeader ( window . location . protocol + endpoint , 'GET' ) } ,
40
- } ) ;
41
- for ( const s of await response . json ( ) ) {
42
- sites . push ( s ) ;
43
- }
50
+ async function saveSite ( domain ) {
51
+ let endpoint = `${ API_BASE_URL } /api/sites` ;
52
+ let response = await fetch ( endpoint , {
53
+ method : 'POST' ,
54
+ headers : {
55
+ 'Accept' : 'application/json' ,
56
+ 'Content-Type' : 'application/json' ,
57
+ 'Authorization' : await getNostrAuthHeader ( window . location . protocol + endpoint , 'POST' ) ,
58
+ } ,
59
+ body : JSON . stringify ( {
60
+ domain : domain ,
61
+ } ) ,
62
+ } ) ;
63
+ return response . ok ;
64
+ }
65
+
66
+ function getApiBaseUrl ( ) {
67
+ if ( API_BASE_URL . startsWith ( "//localhost:" ) ) {
68
+ return `http:${ API_BASE_URL } ` ;
69
+ } else {
70
+ return `${ window . location . protocol } //${ ADMINISTERED_HOST } ` ;
44
71
}
72
+ }
45
73
46
- async function saveSite ( domain ) {
47
- let endpoint = `${ API_BASE_URL } /api/sites` ;
48
- let response = await fetch ( endpoint , {
49
- method : 'POST' ,
50
- headers : {
51
- 'Accept' : 'application/json' ,
52
- 'Content-Type' : 'application/json' ,
53
- 'Authorization' : await getNostrAuthHeader ( window . location . protocol + endpoint , 'POST' ) ,
54
- } ,
55
- body : JSON . stringify ( {
56
- domain : domain ,
57
- } ) ,
58
- } ) ;
74
+ function getApiExtraHeaders ( ) {
75
+ if ( API_BASE_URL . startsWith ( "//localhost:" ) ) {
76
+ return { "X-Target-Host" : ADMINISTERED_HOST } ;
77
+ } else {
78
+ return { } ;
59
79
}
80
+ }
60
81
61
- function getSiteApiBaseUrl ( siteDomain ) {
62
- if ( API_BASE_URL . startsWith ( "//localhost:" ) ) {
63
- return `http:${ API_BASE_URL } ` ;
64
- } else {
65
- return `${ window . location . protocol } //${ siteDomain } ` ;
66
- }
82
+ async function getConfig ( themes ) {
83
+ while ( ! window . nostr ) {
84
+ await new Promise ( r => setTimeout ( r , 0 ) ) ;
67
85
}
68
86
69
- async function getConfig ( site , themes ) {
70
- themes . length = 0 ;
71
-
72
- let endpoint = `${ getSiteApiBaseUrl ( site . domain ) } /api/config` ;
73
- let res = await fetch ( new URL ( endpoint ) , {
74
- headers : { authorization : await getNostrAuthHeader ( endpoint , 'GET' ) }
75
- } ) ;
76
- let configResponse = await res . json ( ) ;
77
- endpoint = `${ getSiteApiBaseUrl ( site . domain ) } /api/themes` ;
78
- res = await fetch ( new URL ( endpoint ) , {
79
- headers : { authorization : await getNostrAuthHeader ( endpoint , 'GET' ) }
80
- } ) ;
81
- let themesResponse = await res . json ( ) ;
82
- for ( t of themesResponse [ 'themes' ] ) {
83
- themes . push ( { name : t . name , license : t . license , description : t . description , selected : t . name == configResponse [ 'theme' ] } ) ;
84
- }
85
- themes . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ;
87
+ themes . length = 0 ;
88
+
89
+ let endpoint = `${ getApiBaseUrl ( ) } /api/config` ;
90
+ let headers = { ...{ authorization : await getNostrAuthHeader ( endpoint , 'GET' ) } , ...getApiExtraHeaders ( ) } ;
91
+ let res = await fetch ( new URL ( endpoint ) , { headers : headers } ) ;
92
+ let configResponse = await res . json ( ) ;
93
+ endpoint = `${ getApiBaseUrl ( ) } /api/themes` ;
94
+ res = await fetch ( new URL ( endpoint ) , {
95
+ headers : { authorization : await getNostrAuthHeader ( endpoint , 'GET' ) }
96
+ } ) ;
97
+ let themesResponse = await res . json ( ) ;
98
+ for ( t of themesResponse [ 'themes' ] ) {
99
+ themes . push ( { name : t . name , license : t . license , description : t . description , selected : t . name == configResponse [ 'theme' ] } ) ;
86
100
}
101
+ themes . sort ( ( a , b ) => a . name . localeCompare ( b . name ) ) ;
102
+ }
87
103
88
- async function saveConfig ( site , themes , desiredThemeName ) {
89
- let endpoint = `${ getSiteApiBaseUrl ( site . domain ) } /api/config` ;
90
- let response = await fetch ( endpoint , {
91
- method : 'PUT' ,
92
- headers : {
93
- 'Accept' : 'application/json' ,
94
- 'Content-Type' : 'application/json' ,
95
- 'Authorization' : await getNostrAuthHeader ( endpoint , 'PUT' ) ,
96
- } ,
97
- body : JSON . stringify ( {
98
- theme : desiredThemeName ,
99
- } ) ,
100
- } ) ;
101
- if ( response . ok ) {
102
- for ( let theme of themes ) {
103
- theme . selected = theme . name === desiredThemeName ;
104
- }
105
- } else {
106
- alert ( "Error changing theme!" ) ;
104
+ async function saveConfig ( themes , desiredThemeName ) {
105
+ let endpoint = `${ getApiBaseUrl ( ) } /api/config` ;
106
+ let headers = {
107
+ ...{
108
+ 'Accept' : 'application/json' ,
109
+ 'Content-Type' : 'application/json' ,
110
+ 'Authorization' : await getNostrAuthHeader ( endpoint , 'PUT' ) ,
111
+ } ,
112
+ ...getApiExtraHeaders ( )
113
+ } ;
114
+ let response = await fetch ( endpoint , {
115
+ method : 'PUT' ,
116
+ headers : headers ,
117
+ body : JSON . stringify ( { theme : desiredThemeName } ) ,
118
+ } ) ;
119
+ if ( response . ok ) {
120
+ for ( let theme of themes ) {
121
+ theme . selected = theme . name === desiredThemeName ;
107
122
}
123
+ } else {
124
+ alert ( "Error changing theme!" ) ;
108
125
}
109
- </ script >
110
- </ head >
111
- < body >
112
- < div class ="w-full mx-auto " x-data ="{site: null, sites: [], themes: []} " x-init ="await getSites(sites); if (sites.length > 0) { site = sites[0]; await getConfig(site, themes); } ">
113
- < div class ="navbar bg-base-200 ">
114
- < div class ="flex-1 ">
115
- < a class ="btn btn-ghost text-xl "> Servus!</ a >
116
- </ div >
117
- < div class ="flex-none ">
118
- < ul class ="menu menu-horizontal px-1 ">
119
- < li >
120
- < details >
121
- < summary > Sites</ summary >
122
- < ul class ="bg-base-100 rounded-t-none p-2 ">
123
- < template x-for ="s in sites ">
124
- < li > < a x-on:click ="site = s " x-text ="s.domain "> </ a > </ li >
125
- </ template >
126
- < li > < a x-on:click ="site = null "> New</ a > </ li >
127
- </ ul >
128
- </ details >
129
- </ li >
130
- </ ul >
131
- </ div >
132
- </ div > <!-- /navbar -->
133
- < div >
134
- < template x-if ="!site ">
135
- < div class ="flex items-center justify-center ">
136
- < div class ="w-3/4 mt-24 " x-data ="{domain: ''} ">
137
- < div class ="form-control ">
138
- < label class ="label " for ="domain ">
139
- < span class ="label-text "> Domain</ span >
140
- </ label >
141
- < input x-model ="domain " type ="text " name ="domain " class ="input input-bordered input-lg " />
142
- </ div >
143
- < div class ="w-full flex justify-center items-center mt-2 ">
144
- < div class ="w-1/2 flex justify-center items-center gap-2 ">
145
- < button x-on:click ="await saveSite(domain); await getSites(sites); site = sites[0]; " class ="btn btn-primary mt-1 "> Save</ button >
126
+ }
127
+ </ script >
128
+ </ head >
129
+
130
+ < body >
131
+ < div x-data ="{host: ADMINISTERED_HOST} ">
132
+ < template x-if ="!host ">
133
+ < div x-data ="{sites: []} " x-init ="(async () => { await getSites(sites); })() ">
134
+ < div class ="mt-12 ">
135
+ < div class ="w-full mt-24 ">
136
+ < div class ="flex flex-wrap gap-4 ">
137
+ < template x-for ="site in sites " :key ="site.domain ">
138
+ < div class ="card w-96 card-xs shadow-sm bg-neutral text-neutral-content ">
139
+ < div class ="card-body ">
140
+ < h2 class ="card-title " x-text ="site.domain "> </ h2 >
141
+ < div class ="justify-end card-actions ">
142
+ < a class ="btn btn-primary " :href ="'/.admin/' + site.domain "> Go</ a >
143
+ </ div >
144
+ </ div >
146
145
</ div >
146
+ </ template >
147
+ < div class ="card w-96 card-xs shadow-sm bg-neutral text-neutral-content "
148
+ x-data ="{domain: '', editing: false} ">
149
+ < template x-if ="!editing ">
150
+ < div class ="card-body ">
151
+ < div class ="justify-end card-actions ">
152
+ < a class ="btn btn-primary " x-on:click ="editing = true; "> New site</ a >
153
+ </ div >
154
+ </ div >
155
+ </ template >
156
+ < template x-if ="editing ">
157
+ < div class ="card-body ">
158
+ < label class ="label " for ="domain ">
159
+ < span class ="label-text "> Domain</ span >
160
+ </ label >
161
+ < input x-model ="domain " type ="text " name ="domain "
162
+ class ="input input-bordered input-lg " />
163
+ < div class ="justify-end card-actions ">
164
+ < a class ="btn btn-primary "
165
+ x-on:click ="if (await saveSite(domain)) { domain = ''; editing = false; await getSites(sites); } else { alert('Error saving site!'); } "> Save</ a >
166
+ < a class ="btn " x-on:click ="domain = ''; editing = false "> Cancel</ a >
167
+ </ div >
168
+ </ div >
169
+ </ template >
147
170
</ div >
148
171
</ div >
149
172
</ div >
150
- </ template > <!-- /!site -->
151
- < template x-if ="site ">
152
-
153
- < div class ="mt-12 ">
154
- < h1 class ="text-2xl text-center " x-text ="site.domain "> </ h1 >
155
- < div class ="w-full mt-24 ">
156
- < div class ="grid gap-4 justify-between grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 ">
157
- < template x-for ="theme in themes ">
158
- < div :class ="theme.selected ? 'bg-primary text-primary-content' : 'bg-neutral text-neutral-content' " class ="card w-96 card-xs shadow-sm ">
159
- < div class ="card-body ">
160
- < h2 class ="card-title " x-text ="theme.name "> </ h2 >
161
- < p x-text ="theme.description "> </ p >
162
- < p > License: < span x-text ="theme.license "> </ span > </ p >
163
- < div class ="justify-end card-actions ">
164
- < button :class ="{'btn-primary': !theme.selected} " class ="btn " x-on:click ="let changed = await saveConfig(site, themes, theme.name); if (changed) { } "> Select</ button >
165
- </ div >
166
- </ div >
173
+ </ div >
174
+ </ div >
175
+ </ template >
176
+ < template x-if ="host ">
177
+ < div class ="w-full mx-auto " x-data ="{themes: []} " x-init ="await getConfig(themes); ">
178
+ < div >
179
+ < div class ="mt-12 ">
180
+ < div class ="w-full mt-24 ">
181
+ < div class ="flex flex-wrap gap-4 ">
182
+ < template x-for ="theme in themes ">
183
+ < div :class ="theme.selected ? 'bg-primary text-primary-content' : 'bg-neutral text-neutral-content' "
184
+ class ="card w-96 card-xs shadow-sm ">
185
+ < div class ="card-body ">
186
+ < h2 class ="card-title " x-text ="theme.name "> </ h2 >
187
+ < p x-text ="theme.description "> </ p >
188
+ < p > License: < span x-text ="theme.license "> </ span > </ p >
189
+ < div class ="justify-end card-actions ">
190
+ < button :class ="{'btn-primary': !theme.selected} " class ="btn "
191
+ x-on:click ="await saveConfig(themes, theme.name); "> Select</ button >
167
192
</ div >
168
- </ template >
169
- </ div >
193
+ </ div >
194
+ </ div >
195
+ </ template >
170
196
</ div >
171
197
</ div >
172
- </ template > <!-- /site -->
173
- </ div > <!-- /main -->
174
- </ div >
175
- </ body >
176
- </ html >
198
+ </ div >
199
+ </ div >
200
+ </ div >
201
+ </ template >
202
+ </ div >
203
+ </ body >
204
+
205
+ </ html >
0 commit comments