Skip to content

Commit 28bdf11

Browse files
committed
Built-in Web Frontend
* Created a Makefile and a script for fast and easy deployment of the site design document * Added documentation how to compile the site design document * The RPC server now supports www-form-urlencoded requests and JSONP for the User ID request sent by the web frontend Misc: * Moved all design documents out of the code and into separate files in apps/ecoinpool/priv * Implemented functions for checking if all design documents exist
1 parent 30b0c7c commit 28bdf11

26 files changed

+4821
-106
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
*.swp
44
*.o
55
*.so
6+
*.state
67
erl_crash.dump
78

89
.eunit
910
deps/
1011
ebin/
12+
scrapped/
13+
14+
.sass-cache
15+
site/compiler.jar
1116

1217
rel/ecoinpool*/

apps/ecoinpool/priv/main_db_auth.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"_id": "_design/auth",
3+
"language": "javascript",
4+
"validate_doc_update": "function(newDoc, oldDoc, userCtx) {\n switch ((newDoc._deleted ? oldDoc.type : newDoc.type)) {\n case 'configuration':\n if (userCtx.roles.indexOf('_admin') === -1)\n throw({forbidden: 'Only admins may edit the root configuration'});\n break;\n case 'sub-pool':\n if (userCtx.roles.indexOf('_admin') === -1)\n throw({forbidden: 'Only admins may edit Subpool configurations'});\n break;\n case 'worker':\n if (!newDoc._deleted) {\n if (newDoc.sub_pool_id === undefined)\n throw({forbidden: 'The sub_pool_id field is missing'});\n if (typeof newDoc.name != 'string' || newDoc.name == '')\n throw({forbidden: 'The name field is missing or invalid'});\n if (userCtx.roles.indexOf('_admin') === -1) {\n if (oldDoc !== null) {\n if (oldDoc.sub_pool_id != newDoc.sub_pool_id)\n throw({forbidden: 'You cannot change the sub_pool_id field'});\n else if (oldDoc.user_id != newDoc.user_id)\n throw({forbidden: 'You cannot change the user_id field'});\n }\n if (typeof newDoc.user_id !== 'number')\n throw({forbidden: 'user_id must be a number'});\n if (userCtx.roles.indexOf('user_id:' + newDoc.sub_pool_id + ':' + newDoc.user_id) === -1)\n throw({forbidden: 'You may only create or update your own workers'});\n if (newDoc.name != userCtx.name && (newDoc.name.indexOf('_') === -1 || newDoc.name.substr(0, newDoc.name.indexOf('_')) != userCtx.name))\n throw({forbidden: 'Worker names must have the format \"username[_suffix]\"'});\n }\n }\n else if (userCtx.roles.indexOf('_admin') === -1) {\n if (userCtx.roles.indexOf('user_id:' + oldDoc.sub_pool_id + ':' + oldDoc.user_id) === -1)\n throw({forbidden: 'You may only delete your own workers'});\n }\n break;\n default:\n throw({forbidden: 'Invalid document type'});\n }\n}"
5+
}
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"_id": "_design/doctypes",
3+
"language": "javascript",
4+
"views": {
5+
"doctypes": {
6+
"map": "function (doc) {if (doc.type !== undefined) emit(doc.type, doc._id);}"
7+
}
8+
},
9+
"filters": {
10+
"pool_only": "function (doc, req) {return doc._deleted || doc.type == 'configuration' || doc.type == 'sub-pool';}",
11+
"workers_only": "function (doc, req) {return doc._deleted || doc.type == 'worker';}"
12+
}
13+
}

apps/ecoinpool/priv/main_db_site.json

+42
Large diffs are not rendered by default.
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"_id": "_design/workers",
3+
"language": "javascript",
4+
"views": {
5+
"by_sub_pool": {
6+
"map": "function(doc) {if (doc.type === 'worker') emit(doc.sub_pool_id, doc.name);}"
7+
},
8+
"by_name": {
9+
"map": "function(doc) {if (doc.type === 'worker') emit(doc.name, doc.sub_pool_id);}"
10+
},
11+
"by_sub_pool_and_name": {
12+
"map": "function(doc) {if (doc.type === 'worker') emit([doc.sub_pool_id, doc.name], doc.user_id);}"
13+
},
14+
"by_sub_pool_and_user_id": {
15+
"map": "function(doc) {if (doc.type === 'worker') emit([doc.sub_pool_id, doc.user_id], doc.name);}"
16+
}
17+
}
18+
}
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"_id": "_design/auth",
3+
"language": "javascript",
4+
"validate_doc_update": "function(newDoc, oldDoc, userCtx) {if (userCtx.roles.indexOf('_admin') !== -1) return; else throw({forbidden: 'Only admins may edit the database'});}"
5+
}
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"_id": "_design/stats",
3+
"language": "javascript",
4+
"views": {
5+
"state": {
6+
"map": "function(doc) {emit([doc.state, doc.user_id, doc.worker_id], 1);}",
7+
"reduce": "function(keys, values, rereduce) {return sum(values);}"
8+
},
9+
"rejected": {
10+
"map": "function(doc) {if (doc.state == 'invalid') emit([doc.reject_reason, doc.user_id, doc.worker_id], 1);}",
11+
"reduce": "function(keys, values, rereduce) {return sum(values);}"
12+
},
13+
"workers": {
14+
"map": "function(doc) {var d = [0,0,0]; switch(doc.state) {case 'invalid': d[0] = 1; break; case 'valid': d[1] = 1; break; case 'candidate': d[2] = 1; break;} emit(doc.worker_id, d);}",
15+
"reduce": "function(keys, values, rereduce) {var s = [0,0,0]; for (var i in values) {var value = values[i]; s[0] += value[0]; s[1] += value[1]; s[2] += value[2];} return s;}"
16+
}
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"_id": "_design/timed_stats",
3+
"language": "javascript",
4+
"views": {
5+
"all_valids": {
6+
"map": "function(doc) {if (doc.state == 'valid' || doc.state == 'candidate') emit(doc.timestamp, 1);}",
7+
"reduce": "function(keys, values, rereduce) {return sum(values);}"
8+
},
9+
"valids_per_user": {
10+
"map": "function(doc) {if (doc.state == 'valid' || doc.state == 'candidate') emit([doc.user_id].concat(doc.timestamp), 1);}",
11+
"reduce": "function(keys, values, rereduce) {return sum(values);}"
12+
},
13+
"valids_per_worker": {
14+
"map": "function(doc) {if (doc.state == 'valid' || doc.state == 'candidate') emit([doc.worker_id].concat(doc.timestamp), 1);}",
15+
"reduce": "function(keys, values, rereduce) {return sum(values);}"
16+
},
17+
"worker_last_share": {
18+
"map": "function(doc) {emit(doc.worker_id, [doc.timestamp, doc.state]);}",
19+
"reduce": "function(keys, values, rereduce) {var cmp = function (a, b) {for (var i in a) {if (a[i] != b[i]) return (a[i] < b[i]);} return false;}; var best = null; for (var i in values) {if (best === null || cmp(best[0], values[i][0])) best = values[i];} return best;}"
20+
}
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"_id": "_design/ecoinpool",
3+
"language": "javascript",
4+
"validate_doc_update": "function(newDoc, oldDoc, userCtx) {\n if (newDoc.name && newDoc.name.indexOf(\"_\") != -1)\n throw({forbidden: 'The username must not contain an underscore'});\n}",
5+
"views": {
6+
"user_ids": {
7+
"map": "function(doc) {\n for (var i in doc.roles) {\n var spl = doc.roles[i].split(\":\");\n if (spl.length == 3 && spl[0] == \"user_id\")\n emit([spl[1], doc.name], parseInt(spl[2]));\n }\n}",
8+
"reduce": "function(keys, values, rereduce) {\n var best = null;\n for (var i in values) {\n if (best === null || best < values[i])\n best = values[i];\n }\n return best;\n}"
9+
}
10+
}
11+
}

apps/ecoinpool/src/ecoinpool_db.erl

+36-96
Original file line numberDiff line numberDiff line change
@@ -123,47 +123,23 @@ set_view_update_interval(Seconds) ->
123123
init([{DBHost, DBPort, DBPrefix, DBOptions}]) ->
124124
% Trap exit
125125
process_flag(trap_exit, true),
126-
% Connect to server
126+
% Create server connection
127127
S = couchbeam:server_connection(DBHost, DBPort, DBPrefix, DBOptions),
128-
% Open config database
129-
ConfDb = case couchbeam:db_exists(S, "ecoinpool") of
130-
true ->
131-
{ok, TheConfDb} = couchbeam:open_db(S, "ecoinpool"),
132-
TheConfDb;
133-
_ ->
134-
case couchbeam:create_db(S, "ecoinpool") of
135-
{ok, NewConfDb} -> % Create basic views and auth lock-out
136-
{ok, _} = couchbeam:save_doc(NewConfDb, {[
137-
{<<"_id">>, <<"_design/doctypes">>},
138-
{<<"language">>, <<"javascript">>},
139-
{<<"views">>, {[
140-
{<<"doctypes">>, {[{<<"map">>, <<"function (doc) {if (doc.type !== undefined) emit(doc.type, doc._id);}">>}]}}
141-
]}},
142-
{<<"filters">>, {[
143-
{<<"pool_only">>, <<"function (doc, req) {return doc._deleted || doc.type == \"configuration\" || doc.type == \"sub-pool\";}">>},
144-
{<<"workers_only">>, <<"function (doc, req) {return doc._deleted || doc.type == \"worker\";}">>}
145-
]}}
146-
]}),
147-
{ok, _} = couchbeam:save_doc(NewConfDb, {[
148-
{<<"_id">>, <<"_design/workers">>},
149-
{<<"language">>, <<"javascript">>},
150-
{<<"views">>, {[
151-
{<<"by_sub_pool">>, {[{<<"map">>, <<"function(doc) {if (doc.type === \"worker\") emit(doc.sub_pool_id, doc.name);}">>}]}},
152-
{<<"by_name">>, {[{<<"map">>, <<"function(doc) {if (doc.type === \"worker\") emit(doc.name, doc.sub_pool_id);}">>}]}},
153-
{<<"by_sub_pool_and_name">>, {[{<<"map">>, <<"function(doc) {if (doc.type === \"worker\") emit([doc.sub_pool_id, doc.name], doc.user_id);}">>}]}},
154-
{<<"by_sub_pool_and_user_id">>, {[{<<"map">>, <<"function(doc) {if (doc.type === \"worker\") emit([doc.sub_pool_id, doc.user_id], doc.name);}">>}]}}
155-
]}}
156-
]}),
157-
{ok, _} = couchbeam:save_doc(NewConfDb, {[
158-
{<<"_id">>, <<"_design/auth">>},
159-
{<<"language">>, <<"javascript">>},
160-
{<<"validate_doc_update">>, <<"function(newDoc, oldDoc, userCtx) {if (userCtx.roles.indexOf('_admin') !== -1) return; else throw({forbidden: 'Only admins may edit the database'});}">>}
161-
]}),
162-
log4erl:info(db, "Config database created!"),
163-
NewConfDb;
164-
{error, Error} ->
165-
log4erl:fatal(db, "config_db - couchbeam:open_or_create_db/3 returned an error:~n~p", [Error]), throw({error, Error})
166-
end
128+
% Setup users database
129+
{ok, UsersDb} = couchbeam:open_db(S, "_users"),
130+
check_design_doc({UsersDb, "ecoinpool", "users_db_ecoinpool.json"}),
131+
% Open and setup config database
132+
ConfDb = case couchbeam:open_or_create_db(S, "ecoinpool") of
133+
{ok, DB} ->
134+
lists:foreach(fun check_design_doc/1, [
135+
{DB, "doctypes", "main_db_doctypes.json"},
136+
{DB, "workers", "main_db_workers.json"},
137+
{DB, "auth", "main_db_auth.json"},
138+
{DB, "site", "main_db_site.json"}
139+
]),
140+
DB;
141+
{error, Error} ->
142+
log4erl:fatal(db, "config_db - couchbeam:open_or_create_db/3 returned an error:~n~p", [Error]), throw({error, Error})
167143
end,
168144
% Start config & worker monitor (asynchronously)
169145
gen_server:cast(?MODULE, start_monitors),
@@ -232,63 +208,17 @@ handle_call({get_workers_for_subpools, SubpoolIds}, _From, State=#state{conf_db=
232208
{reply, Workers, State};
233209

234210
handle_call({setup_shares_db, SubpoolName}, _From, State=#state{srv_conn=S}) ->
235-
case couchbeam:db_exists(S, SubpoolName) of
236-
true ->
211+
case couchbeam:open_or_create_db(S, SubpoolName) of
212+
{ok, DB} ->
213+
lists:foreach(fun check_design_doc/1, [
214+
{DB, "stats", "shares_db_stats.json"},
215+
{DB, "timed_stats", "shares_db_timed_stats.json"},
216+
{DB, "auth", "shares_db_auth.json"}
217+
]),
237218
{reply, ok, State};
238-
_ ->
239-
case couchbeam:create_db(S, SubpoolName) of
240-
{ok, DB} ->
241-
{ok, _} = couchbeam:save_doc(DB, {[
242-
{<<"_id">>, <<"_design/stats">>},
243-
{<<"language">>, <<"javascript">>},
244-
{<<"views">>, {[
245-
{<<"state">>, {[
246-
{<<"map">>, <<"function(doc) {emit([doc.state, doc.user_id, doc.worker_id], 1);}">>},
247-
{<<"reduce">>, <<"function(keys, values, rereduce) {return sum(values);}">>}
248-
]}},
249-
{<<"rejected">>, {[
250-
{<<"map">>, <<"function(doc) {if (doc.state == \"invalid\") emit([doc.reject_reason, doc.user_id, doc.worker_id], 1);}">>},
251-
{<<"reduce">>, <<"function(keys, values, rereduce) {return sum(values);}">>}
252-
]}},
253-
{<<"workers">>, {[
254-
{<<"map">>, <<"function(doc) {var d = [0,0,0]; switch(doc.state) {case \"invalid\": d[0] = 1; break; case \"valid\": d[1] = 1; break; case \"candidate\": d[2] = 1; break;} emit(doc.worker_id, d);}">>},
255-
{<<"reduce">>, <<"function(keys, values, rereduce) {var s = [0,0,0]; for (var i in values) {var value = values[i]; s[0] += value[0]; s[1] += value[1]; s[2] += value[2];} return s;}">>}
256-
]}}
257-
]}}
258-
]}),
259-
{ok, _} = couchbeam:save_doc(DB, {[
260-
{<<"_id">>, <<"_design/timed_stats">>},
261-
{<<"language">>, <<"javascript">>},
262-
{<<"views">>, {[
263-
{<<"all_valids">>, {[
264-
{<<"map">>, <<"function(doc) {if (doc.state == \"valid\" || doc.state == \"candidate\") emit(doc.timestamp, 1);}">>},
265-
{<<"reduce">>, <<"function(keys, values, rereduce) {return sum(values);}">>}
266-
]}},
267-
{<<"valids_per_user">>, {[
268-
{<<"map">>, <<"function(doc) {if (doc.state == \"valid\" || doc.state == \"candidate\") emit([doc.user_id].concat(doc.timestamp), 1);}">>},
269-
{<<"reduce">>, <<"function(keys, values, rereduce) {return sum(values);}">>}
270-
]}},
271-
{<<"valids_per_worker">>, {[
272-
{<<"map">>, <<"function(doc) {if (doc.state == \"valid\" || doc.state == \"candidate\") emit([doc.worker_id].concat(doc.timestamp), 1);}">>},
273-
{<<"reduce">>, <<"function(keys, values, rereduce) {return sum(values);}">>}
274-
]}},
275-
{<<"worker_last_share">>, {[
276-
{<<"map">>, <<"function(doc) {emit(doc.worker_id, [doc.timestamp, doc.state]);}">>},
277-
{<<"reduce">>, <<"function(keys, values, rereduce) {var cmp = function (a, b) {for (var i in a) {if (a[i] != b[i]) return (a[i] < b[i]);} return false;}; var best = null; for (var i in values) {if (best === null || cmp(best[0], values[i][0])) best = values[i];} return best;}">>}
278-
]}}
279-
]}}
280-
]}),
281-
{ok, _} = couchbeam:save_doc(DB, {[
282-
{<<"_id">>, <<"_design/auth">>},
283-
{<<"language">>, <<"javascript">>},
284-
{<<"validate_doc_update">>, <<"function(newDoc, oldDoc, userCtx) {if (userCtx.roles.indexOf('_admin') !== -1) return; else throw({forbidden: 'Only admins may edit the database'});}">>}
285-
]}),
286-
log4erl:info(db, "Shares database \"~s\" created!", [SubpoolName]),
287-
{reply, ok, State};
288-
{error, Error} ->
289-
log4erl:error(db, "shares_db - couchbeam:open_or_create_db/3 returned an error:~n~p", [Error]),
290-
{reply, error, State}
291-
end
219+
{error, Error} ->
220+
log4erl:error(db, "shares_db - couchbeam:open_or_create_db/3 returned an error:~n~p", [Error]),
221+
{reply, error, State}
292222
end;
293223

294224
handle_call(_Message, _From, State=#state{}) ->
@@ -502,6 +432,16 @@ code_change(_OldVersion, State=#state{}, _Extra) ->
502432
%% Other functions
503433
%% ===================================================================
504434

435+
check_design_doc({DB, Name, Filename}) ->
436+
case couchbeam:doc_exists(DB, "_design/" ++ Name) of
437+
true ->
438+
ok;
439+
false ->
440+
{ok, SDoc} = file:read_file(filename:join(code:priv_dir(ecoinpool), Filename)),
441+
{ok, _} = couchbeam:save_doc(DB, ejson:decode(SDoc)),
442+
ok
443+
end.
444+
505445
do_update_views(DBS, PID) ->
506446
try
507447
lists:foreach(

0 commit comments

Comments
 (0)