parsav  Check-in [d6024624c6]

Overview
Comment:enable passwords
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: d6024624c61e176bcbd760d0797db07a3a667a0b86456d00ef898418770871c6
User & Date: lexi on 2021-01-08 05:58:30
Other Links: manifest | tags
Context
2021-01-09
07:15
user mgmt and rt improvements check-in: 05af79b909 user: lexi tags: trunk
2021-01-08
05:58
enable passwords check-in: d6024624c6 user: lexi tags: trunk
2021-01-07
20:39
media uploads work now, some types can be viewed check-in: 93aea04a05 user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [cb3e1743a5] to [9c53eed84d].

119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
...
502
503
504
505
506
507
508







509
510
511
512
513
514
515
...
946
947
948
949
950
951
952

953
954
955
956
957
958
959
....
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510


1511

1512
1513
1514
1515
1516
1517
1518
....
1595
1596
1597
1598
1599
1600
1601









1602
1603
1604
1605
1606
1607
1608

	actor_enum_local = {
		params = {}, sql = [[
			select id, nym, handle, origin, bio,
			       null::text, rank, quota, key, epithet,
			       knownsince::bigint,
					'@' || handle,
				   invites
			from parsav_actors where origin is null
			order by nullif(rank,0) nulls last, handle
		]];
	};

	actor_enum = {
		params = {}, sql = [[
................................................................................
	};
	artifact_disclaim = {
		params = {uint64, uint64}, cmd = true, sql = [[
			delete from parsav_artifact_claims where
				uid = $1::bigint and
				rid = $2::bigint
		]];







	};
	artifact_excise_forget = {
		-- delete the blasted thing and pretend it never existed
		params = {uint64}, cmd=true, sql = [[
			delete from parsav_artifacts where id = $1::bigint
		]];
	};
................................................................................
	}) ]
	a.ptr.id = r:int(uint64, row, 0);
	a.ptr.rights = lib.store.rights_default();
	a.ptr.rights.rank = r:int(uint16, row, 6);
	a.ptr.rights.quota = r:int(uint32, row, 7);
	a.ptr.rights.invites = r:int(uint32, row, 12);
	a.ptr.knownsince = r:int(int64,row, 10);

	if r:null(row,8) then
		a.ptr.key.ct = 0 a.ptr.key.ptr = nil
	else
		a.ptr.key = r:bin(row,8)
	end
	a.ptr.origin = origin
	if avia.buf ~= nil then avia:free() end
................................................................................
		var r = queries.auth_enum_uid.exec(src,uid)
		if r.sz == 0 then return [lib.mem.ptr(lib.mem.ptr(lib.store.auth))].null() end
		var ret = lib.mem.heapa([lib.mem.ptr(lib.store.auth)], r.sz)
		for i=0, r.sz do
			var kind = r:_string(i, 1)
			var comment = r:_string(i, 2)
			var a = [ lib.str.encapsulate(lib.store.auth, {
				kind = {`kind.ptr, `kind.ct};
				comment = {`comment.ptr, `comment.ct};
			}) ]
			a.ptr.aid = r:int(uint64, i, 0)


			a.ptr.netmask = r:cidr(i, 3)

			a.ptr.blacklist = r:bool(i, 4)
			ret.ptr[i] = a
		end
		return ret
	end];

	auth_attach_pw = [terra(
................................................................................
		uid: uint64,
		artifact: uint64,
		desc: pstring,
		folder: pstring
	): {}
		queries.artifact_expropriate.exec(src,uid,artifact,desc,folder, lib.osclock.time(nil))
	end];










	artifact_enum_uid = [terra(
		src: &lib.store.source,
		uid: uint64,
		folder: pstring
	)
		var res = queries.artifact_enum_uid.exec(src,uid,folder)







|







 







>
>
>
>
>
>
>







 







>







 







|
|


>
>
|
>







 







>
>
>
>
>
>
>
>
>







119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
...
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
...
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
....
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
....
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628

	actor_enum_local = {
		params = {}, sql = [[
			select id, nym, handle, origin, bio,
			       null::text, rank, quota, key, epithet,
			       knownsince::bigint,
					'@' || handle,
				   invites, avatarid
			from parsav_actors where origin is null
			order by nullif(rank,0) nulls last, handle
		]];
	};

	actor_enum = {
		params = {}, sql = [[
................................................................................
	};
	artifact_disclaim = {
		params = {uint64, uint64}, cmd = true, sql = [[
			delete from parsav_artifact_claims where
				uid = $1::bigint and
				rid = $2::bigint
		]];
	};
	artifact_collect_garbage = {
		params = {}, cmd = true, sql = [[
			delete from parsav_artifacts where
				id not in (select rid from parsav_artifact_claims) and
				content is not null -- avoid stepping on toes of ban mech
		]];
	};
	artifact_excise_forget = {
		-- delete the blasted thing and pretend it never existed
		params = {uint64}, cmd=true, sql = [[
			delete from parsav_artifacts where id = $1::bigint
		]];
	};
................................................................................
	}) ]
	a.ptr.id = r:int(uint64, row, 0);
	a.ptr.rights = lib.store.rights_default();
	a.ptr.rights.rank = r:int(uint16, row, 6);
	a.ptr.rights.quota = r:int(uint32, row, 7);
	a.ptr.rights.invites = r:int(uint32, row, 12);
	a.ptr.knownsince = r:int(int64,row, 10);
	a.ptr.avatarid = r:int(uint64,row, 13);
	if r:null(row,8) then
		a.ptr.key.ct = 0 a.ptr.key.ptr = nil
	else
		a.ptr.key = r:bin(row,8)
	end
	a.ptr.origin = origin
	if avia.buf ~= nil then avia:free() end
................................................................................
		var r = queries.auth_enum_uid.exec(src,uid)
		if r.sz == 0 then return [lib.mem.ptr(lib.mem.ptr(lib.store.auth))].null() end
		var ret = lib.mem.heapa([lib.mem.ptr(lib.store.auth)], r.sz)
		for i=0, r.sz do
			var kind = r:_string(i, 1)
			var comment = r:_string(i, 2)
			var a = [ lib.str.encapsulate(lib.store.auth, {
				kind = {`kind.ptr, `kind.ct+1};
				comment = {`comment.ptr, `comment.ct+1};
			}) ]
			a.ptr.aid = r:int(uint64, i, 0)
			if r:null(i,3)
				then a.ptr.netmask.pv = 0
				else a.ptr.netmask = r:cidr(i, 3)
			end
			a.ptr.blacklist = r:bool(i, 4)
			ret.ptr[i] = a
		end
		return ret
	end];

	auth_attach_pw = [terra(
................................................................................
		uid: uint64,
		artifact: uint64,
		desc: pstring,
		folder: pstring
	): {}
		queries.artifact_expropriate.exec(src,uid,artifact,desc,folder, lib.osclock.time(nil))
	end];

	artifact_disclaim = [terra(
		src: &lib.store.source,
		uid: uint64,
		artifact: uint64
	)
		queries.artifact_disclaim.exec(src,uid,artifact)
		queries.artifact_collect_garbage.exec(src) -- TODO add a config option to change GC strategies, instead of just always running a cycle after an artifact is disclaimed, which is not very efficient
	end];

	artifact_enum_uid = [terra(
		src: &lib.store.source,
		uid: uint64,
		folder: pstring
	)
		var res = queries.artifact_enum_uid.exec(src,uid,folder)

Modified backend/schema/pgsql-views.sql from [ca832d14af] to [25f9d405bc].

54
55
56
57
58
59
60
61

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
	avataruri	text,
	rank		smallint,
	quota		integer,
	key			bytea,
	epithet		text,
	knownsince	bigint,
	xid			text,
	invites		integer

);

create or replace function
pg_temp.parsavpg_translate_actor(parsav_actors)
returns pg_temp.parsavpg_intern_actor as $$
	select
		($1).id,        ($1).nym,  ($1).handle, ($1).origin, ($1).bio,
		($1).avataruri, ($1).rank, ($1).quota,  ($1).key,    ($1).epithet,
		($1).knownsince::bigint,
		coalesce(($1).handle || '@' ||
				(select domain from parsav_servers as s where s.id = ($1).origin),
			'@' || ($1).handle) as xid,
		($1).invites
$$ language sql;

--drop type if exists pg_temp.parsavpg_intern_post;
create type pg_temp.parsavpg_intern_post as (
	-- order is crucially important, and must match the order used
	-- in row_to_actor. names don't matter
	localpost	bool,







|
>












|







54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
	avataruri	text,
	rank		smallint,
	quota		integer,
	key			bytea,
	epithet		text,
	knownsince	bigint,
	xid			text,
	invites		integer,
	avatarid	bigint
);

create or replace function
pg_temp.parsavpg_translate_actor(parsav_actors)
returns pg_temp.parsavpg_intern_actor as $$
	select
		($1).id,        ($1).nym,  ($1).handle, ($1).origin, ($1).bio,
		($1).avataruri, ($1).rank, ($1).quota,  ($1).key,    ($1).epithet,
		($1).knownsince::bigint,
		coalesce(($1).handle || '@' ||
				(select domain from parsav_servers as s where s.id = ($1).origin),
			'@' || ($1).handle) as xid,
		($1).invites, ($1).avatarid
$$ language sql;

--drop type if exists pg_temp.parsavpg_intern_post;
create type pg_temp.parsavpg_intern_post as (
	-- order is crucially important, and must match the order used
	-- in row_to_actor. names don't matter
	localpost	bool,

Modified parsav.md from [7a2c43b008] to [52b33381db].

21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

additional preconfigure dependencies are necessary if you are building directly from trunk, rather than from a release tarball that includes certain build artifacts which need to be embedded in the binary:

* inkscape, for rendering out some of the UI graphics that can't be represented with standard svg
* cwebp (libwebp package), for transforming inkscape PNGs to webp
* sassc, for compiling the SCSS stylesheet into its final CSS

all builds require terra, which, unfortunately, requires installing an older version of llvm, v9 at the latest (which i develop parsav under). with any luck, your distro will be clever enough to package terra and its dependencies properly (it's trivial on nix, tho you'll need to tweak the terra expression to select a more recent llvm package); Arch Linux is one of those distros which is not so clever, and whose (AUR) terra package is totally broken. due to these unfortunate circumstances, terra is distributed not just in source form, but also in the the form of LLVM IR. distributions will also be made in the form of tarballed object code and assembly listings for various common platforms, currently including x86-32/64, arm7hf, aarch64, riscv, mips32/64, and ppc64/64le.

i've noticed that terra (at least with llvm9) seems to get a bit cantankerous and trigger llvm to fail with bizarre errors when you try to cross-compile parsav from x86-64 to any other platform, even x86-32. i don't know if this problem exists on other architectures or in what form, but as a workaround, the current cross-compile process consists of generating LLVM IR (ostensibly for x86-64, though this is in reality an architecture-independent language), and then compiling that down to an object file with llc. this is an enormous hassle; hopefully the terra (or llvm?) people will fix this eventually.

also note that, while parsav has a flag to build with ASAN, ASAN has proven unusable for most purposes as it routinely reports false positive buffer-heap-overflows. if you figure out how to defuckulate this, i will be overjoyed.

## building

first, either install any missing dependencies as shared libraries, or build them as static libraries with the command `make dep.$LIBRARY`. as a shortcut, `make dep` will build all dependencies as static libraries. note that if the build system finds a static version of a library in the `lib/` folder, it will use that instead of any system library. note that these commands require GNU make (it may be installed as `gmake` on your system), although this is a fairly soft dependency -- if you really need to build it on BSD make, you can probably translate it with a minute or so of work; you'll just have to do some of the various gmake functions' work manually. this may be worthwhile if you're packaging for a BSD.








|

|







21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

additional preconfigure dependencies are necessary if you are building directly from trunk, rather than from a release tarball that includes certain build artifacts which need to be embedded in the binary:

* inkscape, for rendering out some of the UI graphics that can't be represented with standard svg
* cwebp (libwebp package), for transforming inkscape PNGs to webp
* sassc, for compiling the SCSS stylesheet into its final CSS

all builds require terra, which, unfortunately, requires installing an older version of llvm, v9 at the latest (which i develop parsav under). with any luck, your distro will be clever enough to package terra and its dependencies properly (it's trivial on nix, tho you'll need to tweak the terra expression to select a more recent llvm package); Arch Linux is one of those distros which is not so clever, and whose (AUR) terra package is totally broken. due to these unfortunate circumstances, terra is distributed not just in source form, but also in the the form of LLVM IR. 

i've noticed that terra (at least with llvm9) seems to get a bit cantankerous and trigger llvm to fail with bizarre errors when you try to cross-compile parsav from x86-64 to any other platform, even x86-32. i don't know if this problem exists on other architectures or in what form. as a workaround, i've tried generating LLVM IR (ostensibly for x86-64, though this is in reality an architecture-independent language), and then compiling that down to an object file with llc. it doesn't work. the generated binaries seem to run but they crash with bizarre errors and are impossible to debug, as llc refuses to include debug symbols. for these reasons, parsav will (almost certainly) not run on any architecture besides x86-64, at least until terra and/or llvm are fixed.

also note that, while parsav has a flag to build with ASAN, ASAN has proven unusable for most purposes as it routinely reports false positive buffer-heap-overflows. if you figure out how to defuckulate this, i will be overjoyed.

## building

first, either install any missing dependencies as shared libraries, or build them as static libraries with the command `make dep.$LIBRARY`. as a shortcut, `make dep` will build all dependencies as static libraries. note that if the build system finds a static version of a library in the `lib/` folder, it will use that instead of any system library. note that these commands require GNU make (it may be installed as `gmake` on your system), although this is a fairly soft dependency -- if you really need to build it on BSD make, you can probably translate it with a minute or so of work; you'll just have to do some of the various gmake functions' work manually. this may be worthwhile if you're packaging for a BSD.

Modified parsav.t from [7b59e1e979] to [2fffe11917].

39
40
41
42
43
44
45

46












47
48
49
50
51
52
53
54
55
56
57
58




59
60
61
62
63
64
65
66
67
68
69
...
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
				if v.tree.type == ty then return fn(v,...) end
			end
			return (tbl[false])(v,...)
		end)
	end;
	emit_unitary = function(nl,fd,...)
		local code = {}

		for i,v in ipairs{...} do












			if type(v) == 'string' or type(v) == 'number' then
				local str = tostring(v)
				code[#code+1] = `lib.io.send(2, str, [#str])
			elseif type(v) == 'table' and #v == 2 then
				code[#code+1] = `lib.io.send(2, [v[1]], [v[2]])
			elseif v.tree:is 'constant' then
				local str = tostring(v:asvalue())
				code[#code+1] = `lib.io.send(2, str, [#str])
			else
				code[#code+1] = quote var n = v in
					lib.io.send(2, n, lib.str.sz(n)) end
			end




		end
		if nl == true then code[#code+1] = `lib.io.send(fd, '\n', 1)
		elseif nl then code[#code+1] = `lib.io.send(fd, nl, [#nl]) end
		return code
	end;
	emitv = function(nl,fd,...)
		local vec = {}
		local defs = {}
		for i,v in ipairs{...} do
			local str, ct
			if type(v) == 'table' and v.tree and not (v.tree:is 'constant') then
................................................................................
	end;
	osclock = terralib.includec 'time.h';
}
if config.posix then
	lib.uio = terralib.includec 'sys/uio.h';
	lib.emit = lib.emitv -- use more efficient call where available
else lib.emit = lib.emit_unitary end


lib.noise = {
	level = global(uint8,1);
	starttime = global(lib.osclock.time_t);
	lasttime = global(lib.osclock.time_t);
	header = function(code,txt,mod)
		if mod then







>

>
>
>
>
>
>
>
>
>
>
>
>
|
|
<
<
<
|
|
<
<
<
<
|
>
>
>
>



|







 







<







39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61



62
63




64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
...
170
171
172
173
174
175
176

177
178
179
180
181
182
183
				if v.tree.type == ty then return fn(v,...) end
			end
			return (tbl[false])(v,...)
		end)
	end;
	emit_unitary = function(nl,fd,...)
		local code = {}
		local defs = {}
		for i,v in ipairs{...} do
			local str, ct
			if type(v) == 'table' and v.tree and not (v.tree:is 'constant') then
				if v.tree.type.convertible == 'tuple' then
					str = `v._0
					ct = `v._1
				else
					local n = symbol(v.tree.type)
					defs[#defs + 1] = quote var [n] = v end
					str = n
					ct = `lib.str.sz(n)
				end
			else
				if type(v) == 'string' or type(v) == 'number' then
					str = tostring(v) 



				else--if v.tree:is 'constant' then
					str = tostring(v:asvalue())




				end
				ct = ct or #str
			end

			code[#code+1] = `lib.io.send(fd, str, ct)
		end
		if nl == true then code[#code+1] = `lib.io.send(fd, '\n', 1)
		elseif nl then code[#code+1] = `lib.io.send(fd, nl, [#nl]) end
		return quote [defs] in [code] end
	end;
	emitv = function(nl,fd,...)
		local vec = {}
		local defs = {}
		for i,v in ipairs{...} do
			local str, ct
			if type(v) == 'table' and v.tree and not (v.tree:is 'constant') then
................................................................................
	end;
	osclock = terralib.includec 'time.h';
}
if config.posix then
	lib.uio = terralib.includec 'sys/uio.h';
	lib.emit = lib.emitv -- use more efficient call where available
else lib.emit = lib.emit_unitary end


lib.noise = {
	level = global(uint8,1);
	starttime = global(lib.osclock.time_t);
	lasttime = global(lib.osclock.time_t);
	header = function(code,txt,mod)
		if mod then

Modified render/conf.t from [60d6b764a8] to [241a1c4277].

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
..
29
30
31
32
33
34
35





36
37
38

39
40
41
42
43
44
45
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)

local mappings = {
	{url = 'profile', title = 'account profile', render = 'profile'};
	{url = 'avi', title = 'avatar', render = 'avatar'};
	{url = 'ui', title = 'user interface', render = 'ui'};
	{url = 'sec', title = 'security', render = 'sec'};
	{url = 'rel', title = 'relationships', render = 'rel'};
	{url = 'qnt', title = 'quarantine', render = 'quarantine'};
	{url = 'acl', title = 'access control shortcuts', render = 'acl'};
	{url = 'rooms', title = 'chatrooms', render = 'rooms'};
	{url = 'circles', title = 'circles', render = 'circles'};

	{url = 'srv', title = 'server settings', render = 'srv'};
................................................................................

for i, m in ipairs(mappings) do
	if lib.render.conf[m.render] then
		invoker = quote
			if path(1):cmp(lib.str.lit([m.url])) then
				var body = [lib.render.conf[m.render]] (co, path)
				var a: lib.str.acc a:init(body.ct+48)





				a:lpush(['<h1>' .. m.title .. '</h1>']):ppush(body)
				panel = a:finalize()
				body:free()

			else [invoker] end
		end
	end
end

local terra 
render_conf([co], [path], notify: pstr)







|







 







>
>
>
>
>
|
|
|
>







2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
..
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)

local mappings = {
	{url = 'profile', title = 'account profile', render = 'profile'};
	{url = 'avi', title = 'avatar', render = 'avatar'};
	{url = 'ui', title = 'user interface', render = 'ui'};
	{url = 'sec', title = 'security', render = 'sec_overlay'};
	{url = 'rel', title = 'relationships', render = 'rel'};
	{url = 'qnt', title = 'quarantine', render = 'quarantine'};
	{url = 'acl', title = 'access control shortcuts', render = 'acl'};
	{url = 'rooms', title = 'chatrooms', render = 'rooms'};
	{url = 'circles', title = 'circles', render = 'circles'};

	{url = 'srv', title = 'server settings', render = 'srv'};
................................................................................

for i, m in ipairs(mappings) do
	if lib.render.conf[m.render] then
		invoker = quote
			if path(1):cmp(lib.str.lit([m.url])) then
				var body = [lib.render.conf[m.render]] (co, path)
				var a: lib.str.acc a:init(body.ct+48)
				if not body then
					a:lpush(['<h1>' .. m.title .. ' :: error</h1>' ..
						'<p>the requested resource is not available.</p>'])
					panel = a:finalize()
				else
					a:lpush(['<h1>' .. m.title .. '</h1>']):ppush(body)
					panel = a:finalize()
					body:free()
				end
			else [invoker] end
		end
	end
end

local terra 
render_conf([co], [path], notify: pstr)

Modified render/conf/sec.t from [2ed8642241] to [7f83a40056].

1
2
3

4
5
6
7
8
9
10
11
12
13
14
15

16

17
18
19

20














21



















22
23
24








25
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)

local terra 
render_conf_sec(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr
	var time: lib.store.timepoint = co.who.source:auth_sigtime_user_fetch(co.who.id)
	var tstr: int8[26]
	lib.osclock.ctime_r(&time, &tstr[0])
	var body = data.view.conf_sec {
		lastreset = pstr {
			ptr = &tstr[0], ct = lib.str.sz(&tstr[0])
		}
	}
	
	if co.srv.cfg.credmgd then

		var a: lib.str.acc a:init(768)

		body:append(&a)
		var credmgr = data.view.conf_sec_credmg {
			credlist = '<option>your password</option>'

		}














		credmgr:append(&a)



















		return a:finalize()
	else return body:tostr() end
end








return render_conf_sec



>

|
|









>

>
|
|
<
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>



>
>
>
>
>
>
>
>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
-- vim: ft=terra
local pstr = lib.mem.ptr(int8)
local pref = lib.mem.ref(int8)

local terra 
render_conf_sec(co: &lib.srv.convo, uid: uint64): pstr
	var time: lib.store.timepoint = co.who.source:auth_sigtime_user_fetch(uid)
	var tstr: int8[26]
	lib.osclock.ctime_r(&time, &tstr[0])
	var body = data.view.conf_sec {
		lastreset = pstr {
			ptr = &tstr[0], ct = lib.str.sz(&tstr[0])
		}
	}
	
	if co.srv.cfg.credmgd then
		var new = co:pgetv('new')
		var a: lib.str.acc a:init(768)
		if not new then
			body:append(&a)
			var credmgr = data.view.conf_sec_credmg {

				credlist = pstr{'',0};
			}
			var creds = co.srv:auth_enum_uid(uid)
			if creds.ct > 0 then defer creds:free()
				var cl: lib.str.acc cl:init(256)
				for i=0, creds.ct do var c = creds(i).ptr
					if not c.blacklist then
						cl:lpush('<option value="'):shpush(c.aid):lpush('"> ['):push(c.kind,0):lpush('] '):push(c.comment,0)
						if c.netmask.pv ~= 0 then
							-- push string rep
						end
						cl:lpush('</option>')
					end
				end
				credmgr.credlist = cl:finalize()
			end
			credmgr:append(&a)
			if credmgr.credlist.ct > 0 then credmgr.credlist:free() end
		elseif new:cmp(lib.str.plit'pw') then
			var d: data.view.conf_sec_pwnew
			var time = lib.osclock.time(nil)
			var timestr: int8[26] lib.osclock.ctime_r(&time, &timestr[0])
			var cmt: lib.str.acc
			cmt:init(48):lpush('enrolled over http on '):push(&timestr[0],0)
			d.comment = cmt:finalize()

			var st = d:tostr()
			d.comment:free()
			return st
		elseif new:cmp(lib.str.plit'challenge') then
		-- we're going to break the rules a bit and do database munging from
		-- the rendering code, because doing otherwise in this case would be
		-- genuinely nightmarish
		elseif new:cmp(lib.str.plit'otp') then
		elseif new:cmp(lib.str.plit'api') then
		else return pstr.null() end
		return a:finalize()
	else return body:tostr() end
end

terra lib.render.conf.sec_overlay
(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr
 -- render the credential panel for the current user, allowing
 -- it to be reused in the administration UI
	return render_conf_sec(co,co.who.id)
end

return render_conf_sec

Modified render/conf/users.t from [0f343f8c98] to [41f55e0682].

103
104
105
106
107
108
109





110
111
112
113
114
115
116
...
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169










170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235















236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259

260
261





262
263


264

265
266
267
268
269
270
271
		if punct ~= nil then a:push(punct, 1) end
		a:ipush(rnd(uint16,0,65535))
	end

	if xXx then a:lpush('_xXx') end

end






local push_num_field = macro(function(acc,name,lbl,min,max,value,disable)
	name = name:asvalue()
	lbl = lbl:asvalue()
	local start = '<div class="elem small">'
	local enabled = start .. string.format('<label for="%s">%s</label><input type="number" id="%s" name="%s" min="', name, lbl, name, name)
	local disabled = start .. string.format('<label>%s</label><div class="txtbox">', lbl)
................................................................................
local push_checkbox = input_pusher('checkbox',true,false)
local push_pickbox = input_pusher('checkbox',false,false)
local push_radio = input_pusher('radio',false,true)

local mode_local, mode_remote, mode_staff, mode_peers, mode_peons, mode_all = 0,1,2,3,4,5
local terra 
render_conf_users(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr
	if path.ct == 3 then
		var uid, ok = lib.math.shorthand.parse(path(2).ptr,path(2).ct)
		if not ok then goto e404 end
		var user = co.srv:actor_fetch_uid(uid)
		-- FIXME allow xids as well, for manual queries
		if not user then goto e404 end
		defer user:free()
		if not co.who:overpowers(user.ptr) then goto e403 end











		var cinp: lib.str.acc cinp:init(256)
		var clnk: lib.str.acc clnk:init(512)
		cinp:lpush('<div class="elem-group">')
		if user.ptr.rights.rank > 0 and (co.who.rights.powers.elevate() or co.who.rights.powers.demote()) then
			var max = co.who.rights.rank
			if not co.who.rights.powers.elevate() then max = user.ptr.rights.rank end
			var min = co.srv.cfg.nranks
			if not co.who.rights.powers.demote() then min = user.ptr.rights.rank end

			push_num_field(cinp, 'rank', 'rank', max, min, user.ptr.rights.rank, user.ptr.id == co.who.id)
		end
		if co.who.rights.powers.herald() then
			var sanitized: pstr
			if user.ptr.epithet == nil
				then sanitized = pstr {ptr='', ct=0}
				else sanitized = lib.html.sanitize(cs(user.ptr.epithet),true)
			end
			cinp:lpush('<div class="elem"><label for="epithet">epithet</label><input type="text" id="epithet" name="epithet" value="'):ppush(sanitized):lpush('"></div>')
			if user.ptr.epithet ~= nil then sanitized:free() end
		end
		if co.who.rights.powers.invite() or co.who.rights.powers.discipline() then
			var min: uint32 = 0
			if not (co.who.rights.powers.discipline() or
				co.who.rights.powers.demote() and co.who.rights.powers.invite())
					then min = user.ptr.rights.invites end
			var max: uint32 = co.srv.cfg.maxinvites
			if not co.who.rights.powers.invite() then max = user.ptr.rights.invites end

			push_num_field(cinp, 'invites', 'invites', min, max, user.ptr.rights.invites, false)
		end
		if co.who.rights.powers.elevate() or co.who.rights.powers.demote() then
			var max: uint32 = 5000
			if not co.who.rights.powers.elevate() then max = user.ptr.rights.quota end
			var min: uint32 = 0
			if not co.who.rights.powers.demote() then min = user.ptr.rights.quota end

			push_num_field(cinp, 'quota', 'quota', min, max, user.ptr.rights.quota, user.ptr.id == co.who.id and co.who.rights.rank ~= 1)
		end
		cinp:lpush('</div><div class="elem"><div class="check-panel">')

		if user.ptr.id ~= co.who.id and
		   ((user.ptr.rights.rank == 0 and co.who.rights.powers.elevate()) or
		    (user.ptr.rights.rank >  0 and co.who.rights.powers.demote())) then
			push_checkbox(&cinp, 'staff', pstr.null(), 'site staff member', user.ptr.rights.rank > 0, true, pstr.null())
		end

		cinp:lpush('</div></div>')

		if (co.who.rights.powers.elevate() or
		   co.who.rights.powers.demote()) and user.ptr.id ~= co.who.id then
			var map = array([lib.store.privmap])
			cinp:lpush('<details><summary>powers</summary><div class="pick-list">')
				for i=0, [map.type.N] do
					if (co.who.rights.powers and map[i].priv):sz() > 0 then
						var on = (user.ptr.rights.powers and map[i].priv):sz() > 0
						var enabled = (     on  and co.who.rights.powers.demote() ) or
									  ((not on) and co.who.rights.powers.elevate())
						var namea: lib.str.acc namea:compose('power-', map[i].name)
						var name = namea:finalize()
						push_pickbox(&cinp, name, pstr.null(), map[i].name, on, enabled, pstr.null())
						name:free()
					end
				end
			cinp:lpush('</div></details>')
		end
















		-- TODO black mark system? e.g. resolution option for badthink reports
		-- adds a black mark to the offending user; they can be automatically banned
		-- or brought up for review after a certain number of offenses; possibly lower
		-- set of default privs for marked users

		var cinpp = cinp:finalize() defer cinpp:free()
		var clnkp: pstr
		if clnk.sz > 0 then clnkp = clnk:finalize() else
			clnk:free()
			clnkp = pstr { ptr='', ct=0 }
		end
		var unym: lib.str.acc unym:init(64)
		unym:lpush('<a href="/')
		if user(0).origin ~= 0 then unym:lpush('@') end
		do var sanxid = lib.html.sanitize(user(0).xid, true)
			unym:ppush(sanxid)
			sanxid:free() end
		unym:lpush('" class="id">')
		lib.render.nym(user.ptr,0,&unym,false)
		unym:lpush('</a>')
		var pg = data.view.conf_user_ctl {
			name = unym:finalize();
			inputcontent = cinpp;
			linkcontent = clnkp;

		}
		var ret = pg:tostr()





		pg.name:free()
		if clnkp.ct > 0 then clnkp:free() end


		return ret

	else
		var modes = array(P'local', P'remote', P'staff', P'titled', P'peons', P'all')
		var idbuf: int8[lib.math.shorthand.maxlen]
		var ulst: lib.str.acc ulst:init(256)
		var mode: uint8 = mode_local
		var modestr = co:pgetv('show')
		ulst:lpush('<div style="text-align: right"><em>showing ')







>
>
>
>
>







 







|








>
>
>
>
>
>
>
>
>
>
|
<
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|

|
|
|

|
|
|
|
|

|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
|

|
<
<
<
<
<
|
|
|
|
|
|
|
|
|
|
|
|
<
>
|
<
>
>
>
>
>
|
<
>
>
|
>







103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
...
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185

186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270





271
272
273
274
275
276
277
278
279
280
281
282

283
284

285
286
287
288
289
290

291
292
293
294
295
296
297
298
299
300
301
		if punct ~= nil then a:push(punct, 1) end
		a:ipush(rnd(uint16,0,65535))
	end

	if xXx then a:lpush('_xXx') end

end

local terra 
suggest_domain(a: &lib.str.acc)
	var tlds = array('tld','club','town','space','xxx')
end

local push_num_field = macro(function(acc,name,lbl,min,max,value,disable)
	name = name:asvalue()
	lbl = lbl:asvalue()
	local start = '<div class="elem small">'
	local enabled = start .. string.format('<label for="%s">%s</label><input type="number" id="%s" name="%s" min="', name, lbl, name, name)
	local disabled = start .. string.format('<label>%s</label><div class="txtbox">', lbl)
................................................................................
local push_checkbox = input_pusher('checkbox',true,false)
local push_pickbox = input_pusher('checkbox',false,false)
local push_radio = input_pusher('radio',false,true)

local mode_local, mode_remote, mode_staff, mode_peers, mode_peons, mode_all = 0,1,2,3,4,5
local terra 
render_conf_users(co: &lib.srv.convo, path: lib.mem.ptr(pref)): pstr
	if path.ct >= 3 then
		var uid, ok = lib.math.shorthand.parse(path(2).ptr,path(2).ct)
		if not ok then goto e404 end
		var user = co.srv:actor_fetch_uid(uid)
		-- FIXME allow xids as well, for manual queries
		if not user then goto e404 end
		defer user:free()
		if not co.who:overpowers(user.ptr) then goto e403 end

		if path.ct == 4 then
			if path(3):cmp(lib.str.lit'cred') then
				var pg: lib.str.acc pg:init(1024)
				pg:lpush('<div class="context">editing credentials for user <a href="/conf/users/'):rpush(path(2)):lpush('">'):push(user(0).xid,0):lpush('</a></div>')
				var credmgr = lib.render.conf.sec(co, uid)
				pg:ppush(credmgr)
				credmgr:free()
				return pg:finalize()
			else goto e404 end
		elseif path.ct == 3 then
			var cinp: lib.str.acc cinp:init(256)

			cinp:lpush('<div class="elem-group">')
			if user.ptr.rights.rank > 0 and (co.who.rights.powers.elevate() or co.who.rights.powers.demote()) then
				var max = co.who.rights.rank
				if not co.who.rights.powers.elevate() then max = user.ptr.rights.rank end
				var min = co.srv.cfg.nranks
				if not co.who.rights.powers.demote() then min = user.ptr.rights.rank end

				push_num_field(cinp, 'rank', 'rank', max, min, user.ptr.rights.rank, user.ptr.id == co.who.id)
			end
			if co.who.rights.powers.herald() then
				var sanitized: pstr
				if user.ptr.epithet == nil
					then sanitized = pstr {ptr='', ct=0}
					else sanitized = lib.html.sanitize(cs(user.ptr.epithet),true)
				end
				cinp:lpush('<div class="elem"><label for="epithet">epithet</label><input type="text" id="epithet" name="epithet" value="'):ppush(sanitized):lpush('"></div>')
				if user.ptr.epithet ~= nil then sanitized:free() end
			end
			if co.who.rights.powers.invite() or co.who.rights.powers.discipline() then
				var min: uint32 = 0
				if not (co.who.rights.powers.discipline() or
					co.who.rights.powers.demote() and co.who.rights.powers.invite())
						then min = user.ptr.rights.invites end
				var max: uint32 = co.srv.cfg.maxinvites
				if not co.who.rights.powers.invite() then max = user.ptr.rights.invites end

				push_num_field(cinp, 'invites', 'invites', min, max, user.ptr.rights.invites, false)
			end
			if co.who.rights.powers.elevate() or co.who.rights.powers.demote() then
				var max: uint32 = 5000
				if not co.who.rights.powers.elevate() then max = user.ptr.rights.quota end
				var min: uint32 = 0
				if not co.who.rights.powers.demote() then min = user.ptr.rights.quota end

				push_num_field(cinp, 'quota', 'quota', min, max, user.ptr.rights.quota, user.ptr.id == co.who.id and co.who.rights.rank ~= 1)
			end
			cinp:lpush('</div><div class="elem"><div class="check-panel">')

			if user.ptr.id ~= co.who.id and
			   ((user.ptr.rights.rank == 0 and co.who.rights.powers.elevate()) or
				(user.ptr.rights.rank >  0 and co.who.rights.powers.demote())) then
				push_checkbox(&cinp, 'staff', pstr.null(), 'site staff member', user.ptr.rights.rank > 0, true, pstr.null())
			end

			cinp:lpush('</div></div>')

			if (co.who.rights.powers.elevate() or
			   co.who.rights.powers.demote()) and user.ptr.id ~= co.who.id then
				var map = array([lib.store.privmap])
				cinp:lpush('<details><summary>powers</summary><div class="pick-list">')
					for i=0, [map.type.N] do
						if (co.who.rights.powers and map[i].priv):sz() > 0 then
							var on = (user.ptr.rights.powers and map[i].priv):sz() > 0
							var enabled = (     on  and co.who.rights.powers.demote() ) or
										  ((not on) and co.who.rights.powers.elevate())
							var namea: lib.str.acc namea:compose('power-', map[i].name)
							var name = namea:finalize()
							push_pickbox(&cinp, name, pstr.null(), map[i].name, on, enabled, pstr.null())
							name:free()
						end
					end
				cinp:lpush('</div></details>')
			end

			if co.who.id ~= uid and co.who.rights.powers.purge() then
				var purgeconf: lib.str.acc purgeconf:init(48)
				var purgestrs = array(
					'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'eta', 'nu', 'kappa',
					'emerald', 'carnelian', 'sapphire', 'ruby', 'amethyst', 'glory',
					'hope', 'grace', 'pearl', 'carnation', 'rose', 'peony', 'poppy'
				)
				for i=0,3 do
					purgeconf:push(purgestrs[lib.crypt.random(intptr,0,[purgestrs.type.N])],0)
					if i ~= 2 then purgeconf:lpush('-') end
				end
				cinp:lpush('<details><summary>purge account</summary><p>you have the authority to destroy this account and all its associated content irreversibly and irretrievably. if you really wish to apply such an extreme sanction, enter the confirmation string <strong style="user-select:none">'):push(purgeconf.buf,purgeconf.sz):lpush('</strong> below and press the “alter” button to begin the process.</p><div class="elem"><label for="purge">purge confirmation string</label><input type="text" id="purge" name="purgekey"></div><input type="hidden" name="purgestr" value="'):push(purgeconf.buf,purgeconf.sz):lpush('"></details>')
				purgeconf:free()
			end

			-- TODO black mark system? e.g. resolution option for badthink reports
			-- adds a black mark to the offending user; they can be automatically banned
			-- or brought up for review after a certain number of offenses; possibly lower
			-- set of default privs for marked users

			var cinpp = cinp:finalize() defer cinpp:free()





			var unym: lib.str.acc unym:init(64)
			unym:lpush('<a href="/')
			if user(0).origin ~= 0 then unym:lpush('@') end
			do var sanxid = lib.html.sanitize(user(0).xid, true)
				unym:ppush(sanxid)
				sanxid:free() end
			unym:lpush('" class="id">')
			lib.render.nym(user.ptr,0,&unym,false)
			unym:lpush('</a>')
			var ctlbox = data.view.conf_user_ctl {
				name = unym:finalize();
				inputcontent = cinpp;

				btns = pstr{'',0};
			}

			if co.who.id ~= uid and co.who.rights.powers.cred() then
				ctlbox.btns = lib.str.acc{}:compose('<a class="button" href="/conf/users/',path(2),'/cred">security &amp; credentials</a>'):finalize()
			end
			var pg: lib.str.acc pg:init(512)
			ctlbox:append(&pg)
			ctlbox.name:free()

			if ctlbox.btns.ct > 0 then ctlbox.btns:free() end

			return pg:finalize()
		end
	else
		var modes = array(P'local', P'remote', P'staff', P'titled', P'peons', P'all')
		var idbuf: int8[lib.math.shorthand.maxlen]
		var ulst: lib.str.acc ulst:init(256)
		var mode: uint8 = mode_local
		var modestr = co:pgetv('show')
		ulst:lpush('<div style="text-align: right"><em>showing ')

Modified render/nav.t from [5194b2263f] to [50b4e7c2b2].

3
4
5
6
7
8
9
10
11
12
13
14
15
16
render_nav(co: &lib.srv.convo)
	var t: lib.str.acc t:init(64)
	if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then
		t:lpush(' <a accesskey="t" href="/">timeline</a>')
	end
	if co.who ~= nil then
		t:lpush(' <a accesskey="c" href="/compose">compose</a> <a accesskey="p" href="/'):push(co.who.xid,0)
		t:lpush('">profile</a> <a accesskey="m" href="/media">media</a> <a accesskey="o" href="/conf">configure</a> <a accesskey="d" href="/doc">docs</a> <a accesskey="g" href="/logout">log out</a> <a class="bell" href="/notices">notices</a>')
	else
		t:lpush(' <a accesskey="d" href="/doc">docs</a> <a accesskey="g" href="/login">log in</a>')
	end
	return t:finalize()
end
return render_nav







|






3
4
5
6
7
8
9
10
11
12
13
14
15
16
render_nav(co: &lib.srv.convo)
	var t: lib.str.acc t:init(64)
	if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then
		t:lpush(' <a accesskey="t" href="/">timeline</a>')
	end
	if co.who ~= nil then
		t:lpush(' <a accesskey="c" href="/compose">compose</a> <a accesskey="p" href="/'):push(co.who.xid,0)
		t:lpush('">profile</a> <a accesskey="m" href="/media">media</a> <a accesskey="o" href="/conf">configure</a> <a accesskey="d" href="/doc">docs</a> <a accesskey="g" href="/logout">log out</a> <a class="bell" accesskey="x" href="/notices">notices</a>')
	else
		t:lpush(' <a accesskey="d" href="/doc">docs</a> <a accesskey="g" href="/login">log in</a>')
	end
	return t:finalize()
end
return render_nav

Modified render/tweet.t from [18c58bf27c] to [83917dbbe9].

31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
		author = co.actorcache:insert(co.srv:actor_fetch_uid(p.author)).ptr
	end
	if p.rtdby ~= 0 and retweeter == nil then
		retweeter = co.actorcache:insert(co.srv:actor_fetch_uid(p.rtdby)).ptr
	end

	::foundauth::
	var avistr: lib.str.acc if author.origin == 0 then
		avistr:compose('/avi/',author.handle)
	end
	var timestr: int8[26] lib.osclock.ctime_r(&p.posted, &timestr[0])
	for i=0,26 do if timestr[i] == @'\n' then timestr[i] = 0 break end end -- 🙄

	var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0},false)
	defer bhtml:free()

	var idbuf: int8[lib.math.shorthand.maxlen]







<
<
<







31
32
33
34
35
36
37



38
39
40
41
42
43
44
		author = co.actorcache:insert(co.srv:actor_fetch_uid(p.author)).ptr
	end
	if p.rtdby ~= 0 and retweeter == nil then
		retweeter = co.actorcache:insert(co.srv:actor_fetch_uid(p.rtdby)).ptr
	end

	::foundauth::



	var timestr: int8[26] lib.osclock.ctime_r(&p.posted, &timestr[0])
	for i=0,26 do if timestr[i] == @'\n' then timestr[i] = 0 break end end -- 🙄

	var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0},false)
	defer bhtml:free()

	var idbuf: int8[lib.math.shorthand.maxlen]

Modified route.t from [881176d2e1] to [b4f49ac3f6].

284
285
286
287
288
289
290































291
292
293
294
295
296
297
...
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369






















370
371
372
373
374
375
376
...
380
381
382
383
384
385
386
387

388
389
390
391
392
393
394
...
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
...
453
454
455
456
457
458
459


















460
461
462
463
464
465
466
467
468
469
...
501
502
503
504
505
506
507













508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
...
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
	lib.render.tweet_page(co, path, post.ptr)
	do return end

	::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
	::noauth:: do co:complain(401, 'unauthorized', 'you have not supplied the necessary credentials to perform this operation') return end
end
































terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t)
	var msg = pstring.null()
	-- first things first, do priv checks
	if path.ct >= 1 then
		if not co.who.rights.powers.config() and (
			path(1):cmp(lib.str.lit 'srv')   or
................................................................................
				co.ui_hue = co.srv.cfg.ui_hue
			end

			msg = lib.str.plit 'profile changes saved'
			--user_refresh = true -- not really necessary here, actually

		elseif path(1):cmp(lib.str.lit 'sec') then
			var act = co:ppostv('act')
			if act:cmp(lib.str.plit 'invalidate') then
				lib.dbg('setting user\'s cookie validation time to now')
				co.who.source:auth_sigtime_user_alter(co.who.id, lib.osclock.time(nil))
				-- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again
				co:installkey('/conf/sec',co.aid)
				return
			end
		elseif path(1):cmp(lib.str.lit 'users') then
			if path.ct >= 3 then
				var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct)
				if ok then
					var usr = co.srv:actor_fetch_uid(userid)
					if usr:ref() then defer usr:free()
						if not co.who:overpowers(usr.ptr) then goto nopriv end
					end
				end
			elseif path.ct == 2 then






















			end
		end

		if user_refresh then -- refresh the user info for the renderer
			var usr = co.srv:actor_fetch_uid(co.who.id)
			lib.mem.heapf(co.who)
			co.who = usr.ptr
................................................................................
			co:reroute(go)
			return
		end
	end
	lib.render.conf(co,path,msg)
	do return end

	::nopriv:: co:complain(403,'insufficient privileges','you do not have the necessary powers to perform this action')

end

terra http.user_notices(co: &lib.srv.convo, meth: method.t)
	if meth == method.post then
		var act = co:ppostv('act')
		if act:cmp(lib.str.plit'clear') then
			co.srv:actor_conf_int_set(co.who.id, 'notice-clear-time', lib.osclock.time(nil))
................................................................................

	lib.render.notices(co)
	do return end

	::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
end

terra http.media_manager(co: &lib.srv.convo, path: hpath, meth: method.t)
	if meth == method.post then
		goto badop
	end

	if path.ct == 2 and path(1):cmp(lib.str.lit'upload') and co.who.rights.powers.artifact() then
		if meth == method.get then
			var view = data.view.media_upload {
				folders = ''
			}
			var pg = view:tostr() defer pg:free()
			co:stdpage([lib.srv.convo.page] {
				title = lib.str.plit'media :: upload';
................................................................................
			var idbuf: int8[lib.math.shorthand.maxlen]
			var idlen = lib.math.shorthand.gen(id,&idbuf[0])

			var url = lib.str.acc{}:compose('/media/a/',pstring{&idbuf[0],idlen}):finalize()
			co:reroute(url.ptr)
			url:free()
		else goto badop end


















	else
		if meth == method.post then goto badop end
		lib.render.media_gallery(co,path,co.who.id,nil)
	end
	do return end

	::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
	::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded by this user') return end
end

................................................................................
		::[send]:: page:send(co.con) return true
	end
end


terra http.local_avatar(co: &lib.srv.convo, handle: lib.mem.ptr(int8))
	-- TODO retrieve user avatars













	co:reroute('/s/default-avatar.webp')
end

terra http.file_serve_raw(co: &lib.srv.convo, id: lib.mem.ptr(int8))
	var id, idok = lib.math.shorthand.parse(id.ptr, id.ct)
	if not idok then goto e404 end
	var data, mime = co.srv:artifact_load(id)
	if not data then goto e404 end
	do defer data:free() defer mime:free()
		var safemime = mime
		-- TODO this is not a satisfactory solution; it's a bandaid on a gaping
		-- chest wound. ultimately we need to compile a whitelist of safe mime
		-- types as part of mimelib, but that is no small task. for now, this
		-- will keep the patient from immediately bleeding out
		if mime:cmp(lib.str.plit'text/html') or
			mime:cmp(lib.str.plit'text/xml') or
			mime:cmp(lib.str.plit'application/xhtml+xml') or
			mime:cmp(lib.str.plit'application/vnd.wap.xhtml+xml')
		then -- danger will robinson
			safemime = lib.str.plit'text/plain'
		elseif mime:cmp(lib.str.plit'application/x-shockwave-flash') then
			safemime = lib.str.plit'application/octet-stream'
		end
		lib.net.mg_printf(co.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\nContent-Security-Policy: sandbox; default-src 'none'; form-action 'none'; navigate-to 'none';\r\nX-Content-Options: nosniff\r\n\r\n", safemime.ct, safemime.ptr, data.ct + 2)
		lib.net.mg_send(co.con, data.ptr, data.ct)
		lib.net.mg_send(co.con, '\r\n', 2)
	return end

	::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded to this instance') return end
end

-- entry points
terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
................................................................................
			http.actor_profile_uid(co, path, meth)
		elseif path.ct > 1 and path(0):cmp(lib.str.lit('post')) then
			http.tweet_page(co, path, meth)
		elseif path(0):cmp(lib.str.lit('tl')) then
			http.timeline(co, path)
		elseif path(0):cmp(lib.str.lit('media')) then
			if co.aid == 0 then goto unauth end
			http.media_manager(co, path, meth)
		elseif path(0):cmp(lib.str.lit('doc')) then
			if not meth_get(meth) then goto wrongmeth end
			http.documentation(co, path)
		elseif path(0):cmp(lib.str.lit('conf')) then
			if co.aid == 0 then goto unauth end
			http.configure(co,path,meth)
		else goto notfound end







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







|
<
<
<
<
<
<
<









|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







|
>







 







|
<
<
<
<
|







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


|







 







>
>
>
>
>
>
>
>
>
>
>
>
>
|








|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







 







|







284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
...
376
377
378
379
380
381
382
383







384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
...
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
...
446
447
448
449
450
451
452
453




454
455
456
457
458
459
460
461
...
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
...
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
















592
593
594
595
596
597
598
...
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
	lib.render.tweet_page(co, path, post.ptr)
	do return end

	::badurl:: do co:complain(404, 'invalid URL', 'this URL does not reference extant content or functionality') return end
	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
	::noauth:: do co:complain(401, 'unauthorized', 'you have not supplied the necessary credentials to perform this operation') return end
end

local terra 
credsec_for_uid(co: &lib.srv.convo, uid: uint64)
	var act = co:ppostv('act')
	if act:cmp(lib.str.plit 'invalidate') then
		lib.dbg('setting user\'s cookie validation time to now')
		co.who.source:auth_sigtime_user_alter(uid, lib.osclock.time(nil))
		-- the current session has been invalidated as well, so we need to immediately install a new authentication cookie with the same aid so the user doesn't need to log back in all over again
		co:installkey('/conf/sec',co.aid)
		return
	elseif act:cmp(lib.str.plit 'newcred') then
		var cmt = co:ppostv('comment')
		var pw = co:ppostv('newpw')
		if pw:ref() then
			var cpw = co:ppostv('rptpw')
			if not pw:cmp(cpw) then
				co:complain(400,'enrollment failure','the passwords you supplied do not match')
				return
			end
			co.srv:auth_attach_pw(uid, false, pw, cmt)
			co:reroute('?')
			return
		else
			var key = co:ppostv('newkey')
			if key:ref() then

			end
		end
	end
	co:complain(400,'bad request','the operation you have requested is not meaningful in this context')
end

terra http.configure(co: &lib.srv.convo, path: hpath, meth: method.t)
	var msg = pstring.null()
	-- first things first, do priv checks
	if path.ct >= 1 then
		if not co.who.rights.powers.config() and (
			path(1):cmp(lib.str.lit 'srv')   or
................................................................................
				co.ui_hue = co.srv.cfg.ui_hue
			end

			msg = lib.str.plit 'profile changes saved'
			--user_refresh = true -- not really necessary here, actually

		elseif path(1):cmp(lib.str.lit 'sec') then
			credsec_for_uid(co, co.who.id)







		elseif path(1):cmp(lib.str.lit 'users') then
			if path.ct >= 3 then
				var userid, ok = lib.math.shorthand.parse(path(2).ptr, path(2).ct)
				if ok then
					var usr = co.srv:actor_fetch_uid(userid)
					if usr:ref() then defer usr:free()
						if not co.who:overpowers(usr.ptr) then goto nopriv end
					end
				end
			elseif path.ct == 2 and meth == method.post then
				var act = co:ppostv('act')
				if act:cmp(lib.str.plit'create') then
					var newname = co:ppostv('handle')
					if not newname or not lib.store.actor.handle_validate(newname.ptr) then
						co:complain(400,'invalid handle','the handle you have requested is not valid')
					end
					var tu = co.srv:actor_fetch_xid(newname)
					if tu:ref() then tu:free()
						co:complain(409,'handle clash','that handle conflicts with one that already exists')
						return
					end
					var kbuf: uint8[lib.crypt.const.maxdersz]
					var na = lib.store.actor.mk(&kbuf[0])
					na.handle = newname.ptr
					var newuid = co.srv:actor_create(&na)
					var shid: int8[lib.math.shorthand.maxlen]
					var shidlen = lib.math.shorthand.gen(newuid, &shid[0])
					var url = lib.str.acc{}:compose('/conf/users/',pstring{&shid[0],shidlen}):finalize() defer url:free()
					co:reroute(url.ptr)
					return
				elseif act:cmp(lib.str.plit'inst') then
				else goto badop end
			end
		end

		if user_refresh then -- refresh the user info for the renderer
			var usr = co.srv:actor_fetch_uid(co.who.id)
			lib.mem.heapf(co.who)
			co.who = usr.ptr
................................................................................
			co:reroute(go)
			return
		end
	end
	lib.render.conf(co,path,msg)
	do return end

	::nopriv:: do co:complain(403,'insufficient privileges','you do not have the necessary powers to perform this action') return end
	::badop:: do co:complain(400,'bad request','the operation you have requested is not meaningful in this context') return end
end

terra http.user_notices(co: &lib.srv.convo, meth: method.t)
	if meth == method.post then
		var act = co:ppostv('act')
		if act:cmp(lib.str.plit'clear') then
			co.srv:actor_conf_int_set(co.who.id, 'notice-clear-time', lib.osclock.time(nil))
................................................................................

	lib.render.notices(co)
	do return end

	::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
end

terra http.media_manager(co: &lib.srv.convo, path: hpath, meth: method.t, uid: uint64)




	if co.aid ~= 0 and co.who.id == uid and path.ct == 2 and path(1):cmp(lib.str.lit'upload') and co.who.rights.powers.artifact() then
		if meth == method.get then
			var view = data.view.media_upload {
				folders = ''
			}
			var pg = view:tostr() defer pg:free()
			co:stdpage([lib.srv.convo.page] {
				title = lib.str.plit'media :: upload';
................................................................................
			var idbuf: int8[lib.math.shorthand.maxlen]
			var idlen = lib.math.shorthand.gen(id,&idbuf[0])

			var url = lib.str.acc{}:compose('/media/a/',pstring{&idbuf[0],idlen}):finalize()
			co:reroute(url.ptr)
			url:free()
		else goto badop end
	elseif co.aid ~= 0 and path.ct == 4 and path(1):cmp(lib.str.lit'a') and meth==method.post then 
		var act = co:ppostv('act')
		if not act or not act:cmp(lib.str.plit'confirm') then goto badop end
		var artid, aok = lib.math.shorthand.parse(path(2).ptr,path(2).ct)
		if not aok then goto e404 end
		var art = co.srv:artifact_fetch(uid,artid)
		if not art then goto e404 end
		defer art:free()

		if path(3):cmp(lib.str.lit'avi') then
		 -- user wants to set avatar
			co.who.avatarid = artid
			co.srv:actor_save(co.who)
			co:reroute('/conf/avi')
		elseif path(3):cmp(lib.str.lit'del') then 
			co.srv:artifact_disclaim(co.who.id, artid)
			co:reroute('/media')
		else goto badop end
	else
		if meth == method.post then goto badop end
		lib.render.media_gallery(co,path,uid,nil)
	end
	do return end

	::badop:: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
	::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded by this user') return end
end

................................................................................
		::[send]:: page:send(co.con) return true
	end
end


terra http.local_avatar(co: &lib.srv.convo, handle: lib.mem.ptr(int8))
	-- TODO retrieve user avatars
	var usr = co.srv:actor_fetch_xid(handle)
	if not usr then
	goto default end
	if usr(0).origin == 0 then
		if usr(0).avatarid == 0 then goto default end
		var avi, mime = co.srv:artifact_load(usr(0).avatarid)
		if not avi then goto default end
		defer avi:free() defer mime:free()
		co:bytestream(mime,avi)
	else
		co:reroute(usr(0).avatar)
	end
	do return end
	::default:: co:reroute('/s/default-avatar.webp')
end

terra http.file_serve_raw(co: &lib.srv.convo, id: lib.mem.ptr(int8))
	var id, idok = lib.math.shorthand.parse(id.ptr, id.ct)
	if not idok then goto e404 end
	var data, mime = co.srv:artifact_load(id)
	if not data then goto e404 end
	do defer data:free() defer mime:free()
		co:bytestream(mime,data)
















	return end

	::e404:: do co:complain(404, 'artifact not found', 'no such artifact has been uploaded to this instance') return end
end

-- entry points
terra r.dispatch_http(co: &lib.srv.convo, uri: lib.mem.ptr(int8), meth: method.t)
................................................................................
			http.actor_profile_uid(co, path, meth)
		elseif path.ct > 1 and path(0):cmp(lib.str.lit('post')) then
			http.tweet_page(co, path, meth)
		elseif path(0):cmp(lib.str.lit('tl')) then
			http.timeline(co, path)
		elseif path(0):cmp(lib.str.lit('media')) then
			if co.aid == 0 then goto unauth end
			http.media_manager(co, path, meth, co.who.id)
		elseif path(0):cmp(lib.str.lit('doc')) then
			if not meth_get(meth) then goto wrongmeth end
			http.documentation(co, path)
		elseif path(0):cmp(lib.str.lit('conf')) then
			if co.aid == 0 then goto unauth end
			http.configure(co,path,meth)
		else goto notfound end

Modified srv.t from [ca6d27c8d7] to [56e1fe84a6].

228
229
230
231
232
233
234



















235
236
237
238
239
240
241
		self:rawpage(200, pg, [lib.mem.ptr(lib.http.header)] {
			ptr = &hdrs[0], ct = 3
		})
	end
end

terra convo:stdpage(pg: convo.page) self:statpage(200, pg) end




















terra convo:reroute_cookie(dest: rawstring, cookie: rawstring)
	var hdrs = array(
		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
		lib.http.header { key = 'Location',     value = dest },
		lib.http.header { key = 'Set-Cookie',   value = cookie }
	)







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
		self:rawpage(200, pg, [lib.mem.ptr(lib.http.header)] {
			ptr = &hdrs[0], ct = 3
		})
	end
end

terra convo:stdpage(pg: convo.page) self:statpage(200, pg) end

terra convo:bytestream(mime: pstring, data: lib.mem.ptr(uint8))
	-- TODO this is not a satisfactory solution; it's a bandaid on a gaping
	-- chest wound. ultimately we need to compile a whitelist of safe mime
	-- types as part of mimelib, but that is no small task. for now, this
	-- will keep the patient from immediately bleeding out
	if mime:cmp(lib.str.plit'text/html') or
		mime:cmp(lib.str.plit'text/xml') or
		mime:cmp(lib.str.plit'application/xhtml+xml') or
		mime:cmp(lib.str.plit'application/vnd.wap.xhtml+xml')
	then -- danger will robinson
		mime = lib.str.plit'text/plain'
	elseif mime:cmp(lib.str.plit'application/x-shockwave-flash') then
		mime = lib.str.plit'application/octet-stream'
	end
	lib.net.mg_printf(self.con, "HTTP/1.1 200 OK\r\nContent-Type: %.*s\r\nContent-Length: %llu\r\nContent-Security-Policy: sandbox; default-src 'none'; form-action 'none'; navigate-to 'none';\r\nX-Content-Options: nosniff\r\n\r\n", mime.ct, mime.ptr, data.ct + 2)
	lib.net.mg_send(self.con, data.ptr, data.ct)
	lib.net.mg_send(self.con, '\r\n', 2)
end

terra convo:reroute_cookie(dest: rawstring, cookie: rawstring)
	var hdrs = array(
		lib.http.header { key = 'Content-Type', value = 'text/html; charset=UTF-8' },
		lib.http.header { key = 'Location',     value = dest },
		lib.http.header { key = 'Set-Cookie',   value = cookie }
	)

Modified static/style.scss from [fb57063208] to [f9852f8724].

631
632
633
634
635
636
637









638
639
640
641
642
643
644
}

menu { all: unset; display: block; }
body.conf main {
	display: grid;
	grid-template-columns: 2in 1fr;
	grid-template-rows: max-content 1fr;









	> menu { @extend %navmenu; }
	> .panel {
		grid-column: 2/3; grid-row: 1/3;
		padding-left: 0.15in;
		> h1 {
			padding-bottom: 0.1in;
			margin-bottom: 0.1in;







>
>
>
>
>
>
>
>
>







631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
}

menu { all: unset; display: block; }
body.conf main {
	display: grid;
	grid-template-columns: 2in 1fr;
	grid-template-rows: max-content 1fr;
	div.context {
		border-radius: 4px;
		text-align: center;
		background: tone(-53%);
		box-shadow: 0 1px 0 1px tone(-55%);
		border: 1px solid tone(-20%);
		font-style: italic;
		padding: 0.1in;
	}
	> menu { @extend %navmenu; }
	> .panel {
		grid-column: 2/3; grid-row: 1/3;
		padding-left: 0.15in;
		> h1 {
			padding-bottom: 0.1in;
			margin-bottom: 0.1in;

Modified store.t from [eca94a58d5] to [b8bbc4e0ec].

163
164
165
166
167
168
169

170
171
172
173
174
175
176
...
407
408
409
410
411
412
413

414
415
416
417
418
419
420
421
	var newkp = lib.crypt.genkp()
	var privsz = lib.crypt.der(false,&newkp,kbuf)
	return m.actor {
		id = 0; nym = nil; handle = nil;
		origin = 0; bio = nil; avatar = nil;
		knownsince = lib.osclock.time(nil);
		rights = m.rights_default();

		epithet = nil, key = [lib.mem.ptr(uint8)] {
			ptr = &kbuf[0], ct = privsz
		};
	}
end

struct m.actor_stats {
................................................................................
	actor_notice_enum: {&m.source, uint64} -> lib.mem.ptr(m.notice)
	actor_rel_create: {&m.source, uint16, uint64, uint64} -> {}
	actor_rel_destroy: {&m.source, uint16, uint64, uint64} -> {}
	actor_rel_calc: {&m.source, uint64, uint64} -> m.relationship

	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.lstptr(m.auth)
	auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)

	auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {}
		-- uid: uint64
		-- reset: bool (delete other passwords?)
		-- pw: pstring
		-- comment: pstring
	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
	auth_purge_otp: {&m.source, uint64, rawstring} -> {}
	auth_purge_trust: {&m.source, uint64, rawstring} -> {}







>







 







>
|







163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
...
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
	var newkp = lib.crypt.genkp()
	var privsz = lib.crypt.der(false,&newkp,kbuf)
	return m.actor {
		id = 0; nym = nil; handle = nil;
		origin = 0; bio = nil; avatar = nil;
		knownsince = lib.osclock.time(nil);
		rights = m.rights_default();
		avatarid = 0;
		epithet = nil, key = [lib.mem.ptr(uint8)] {
			ptr = &kbuf[0], ct = privsz
		};
	}
end

struct m.actor_stats {
................................................................................
	actor_notice_enum: {&m.source, uint64} -> lib.mem.ptr(m.notice)
	actor_rel_create: {&m.source, uint16, uint64, uint64} -> {}
	actor_rel_destroy: {&m.source, uint16, uint64, uint64} -> {}
	actor_rel_calc: {&m.source, uint64, uint64} -> m.relationship

	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.lstptr(m.auth)
	auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
	auth_attach_pw:  {&m.source, uint64, bool, pstr, pstr} -> {}
	auth_attach_key: {&m.source, uint64, bool, pstr, pstr} -> {}
		-- uid: uint64
		-- reset: bool (delete other passwords?)
		-- pw: pstring
		-- comment: pstring
	auth_purge_pw: {&m.source, uint64, rawstring} -> {}
	auth_purge_otp: {&m.source, uint64, rawstring} -> {}
	auth_purge_trust: {&m.source, uint64, rawstring} -> {}

Modified view/conf-sec-credmg.tpl from [43efff9618] to [6f4f9fa693].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
..
23
24
25
26
27
28
29
30
31
32
33
34
35
<hr>
<form method="post">
	<p>your account can currently be accessed with the credentials listed below. if you fear a credential has been compromised, you can revoke or reset it.</p>
	<select size="6" name="cred">
		@credlist
	</select>
	<menu class="horizontal choice">
		<button name="act" value="reset">reset</button>
		<button name="act" value="revoke">revoke</button>
	</menu>
</form>
<hr>
<form method="post">
	<p>you can associate extra credentials with your account. you can also limit how much of your authority these credentials can be used to exercise &mdash; for instance, it might be useful to create API keys that can read your timeline, but not post as you or access any administrative powers you may have. if you don't select a capability set, the credential will be able to wield the full scope of your powers.</p>
	<div class="check-panel">
		<label><input type="checkbox" name="allow-post"> post</label>
		<label><input type="checkbox" name="allow-edit"> edit</label>
		<label><input type="checkbox" name="allow-acct"> manage account</label>
		<label><input type="checkbox" name="allow-upload"> upload artifacts</label>
		<label><input type="checkbox" name="allow-censor"> moderation</label>
		<label><input type="checkbox" name="allow-admin"> other admin powers</label>
................................................................................
	</div>
	<p>you can also specify an IP address range in CIDR format to associate with this credential. if you do so, this credential will only be usable when connecting from an IP address in that range. otherwise, it will be valid when connecting from anywhere on the internet.</p>
	<div class="elem">
		<label for="netmask">netmask</label>
		<input type="text" name="netmask" id="netmask" placeholder="10.0.0.0/8">
	</div>
	<menu class="vertical choice">
		<button name="kind" value="pw">new password</button>
		<button name="kind" value="otp">new OTP key</button>
		<button name="kind" value="api">new API token</button>
		<button name="kind" value="challenge">new challenge key</button>
	</div>
</form>


|









|
|







 







|
|
|
|
|

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
..
23
24
25
26
27
28
29
30
31
32
33
34
35
<hr>
<form method="post">
	<p>this account can currently be accessed with the credentials listed below. if you fear a credential has been compromised, you can revoke or reset it.</p>
	<select size="6" name="cred">
		@credlist
	</select>
	<menu class="horizontal choice">
		<button name="act" value="reset">reset</button>
		<button name="act" value="revoke">revoke</button>
	</menu>
</form>
<hr>
<form method="get">
	<p>you can associate extra credentials with this account. you can also limit how much of this account’s authority these credentials can be used to exercise &mdash; for instance, it might be useful to create API keys that can read the account timeline, but not post as the account owner or access any of his administrative powers. if you don't select a capability set, the credential will be able to wield the full scope of the associated account‘s powers.</p>
	<div class="check-panel">
		<label><input type="checkbox" name="allow-post"> post</label>
		<label><input type="checkbox" name="allow-edit"> edit</label>
		<label><input type="checkbox" name="allow-acct"> manage account</label>
		<label><input type="checkbox" name="allow-upload"> upload artifacts</label>
		<label><input type="checkbox" name="allow-censor"> moderation</label>
		<label><input type="checkbox" name="allow-admin"> other admin powers</label>
................................................................................
	</div>
	<p>you can also specify an IP address range in CIDR format to associate with this credential. if you do so, this credential will only be usable when connecting from an IP address in that range. otherwise, it will be valid when connecting from anywhere on the internet.</p>
	<div class="elem">
		<label for="netmask">netmask</label>
		<input type="text" name="netmask" id="netmask" placeholder="10.0.0.0/8">
	</div>
	<menu class="vertical choice">
		<button name="new" value="pw">new password</button>
		<button name="new" value="otp">new OTP key</button>
		<button name="new" value="api">new API token</button>
		<button name="new" value="challenge">new challenge key</button>
	</menu>
</form>

Added view/conf-sec-pwnew.tpl version [5878dd4701].





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<form method="post">
	<div class="elem">
		<label for="comment">comment</label>
		<input type="text" id="comment" name="comment" value="@comment" required>
	</div>
	<div class="elem">
		<label for="newpw">new password</label>
		<input type="password" id="newpw" name="newpw" required>
	</div>
	<div class="elem">
		<label for="rptpw">confirm password</label>
		<input type="password" id="rptpw" name="rptpw" required>
	</div>
	<menu class="choice horizontal">
		<button name="act" value="newcred">enroll</button>
		<a class="button" href="?">cancel</a>
	</menu>
</form>

Modified view/conf-sec.tpl from [de1cf7e8f0] to [49c75b1abd].

1
2
3
4
5
6
7
8
9
10
<form method="post">
	<p>if you are concerned that your account may have been compromised, you can terminate all other login sessions by invalidating their session cookies. note that this will not have any effect on API tokens; these must be revoked separately!</p>
	<div class="elem">
		<label> sessions valid from </label>
		<div class="txtbox">@lastreset</div>
	</div>
	<button type="submit" name="act" value="invalidate">
		invalidate other sessions
	</button>
</form>

|








1
2
3
4
5
6
7
8
9
10
<form method="post">
	<p>if you are concerned that this account may have been compromised, you can terminate conflicting login sessions by invalidating their session cookies. note that this will not have any effect on API tokens; these must be revoked separately!</p>
	<div class="elem">
		<label> sessions valid from </label>
		<div class="txtbox">@lastreset</div>
	</div>
	<button type="submit" name="act" value="invalidate">
		invalidate other sessions
	</button>
</form>

Modified view/conf-user-ctl.tpl from [7abbc95a91] to [7e2cfb9c6e].

1
2
3
4
5
6

7


8
9
<form method="post">
	<div class="elem">
		<label>user</label>
		<div class="txtbox">@name</div>
	</div>
	@inputcontent

	<button>alter</button>


</form>
@linkcontent






>
|
>
>

<
1
2
3
4
5
6
7
8
9
10
11

<form method="post">
	<div class="elem">
		<label>user</label>
		<div class="txtbox">@name</div>
	</div>
	@inputcontent
	<menu class="vertical choice">
		<button>alter</button>
		@btns 
	</menu>
</form>

Modified view/load.lua from [9f4f065de0] to [15344e4760].

19
20
21
22
23
24
25

26
27
28
29
30
31
32
	'login-username';
	'login-challenge';

	'conf';
	'conf-profile';
	'conf-sec';
	'conf-sec-credmg';

	'conf-user-ctl';
}

local ingest = function(filename)
	local hnd = io.open(path..'/'..filename)
	local txt = hnd:read('*a')
	io.close(hnd)







>







19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	'login-username';
	'login-challenge';

	'conf';
	'conf-profile';
	'conf-sec';
	'conf-sec-credmg';
	'conf-sec-pwnew';
	'conf-user-ctl';
}

local ingest = function(filename)
	local hnd = io.open(path..'/'..filename)
	local txt = hnd:read('*a')
	io.close(hnd)