@totemorg/securelink
Version:
provide secure login link to web services
1,169 lines (929 loc) • 29.8 kB
JavaScript
// UNCLASSIFIED
/**
Provides a private (end-to-end encrypted) message link between trusted clients via secure logins.
This module documented in accordance with [jsdoc]{@link https://jsdoc.app/}.
## Env Vars
LINK_PASS = passphrase to encrypt client information
LINK_HOST = @name suffix of guest clients
@module SECLINK
@author [ACMESDS](https://totemorg.github.io)
@requires [enums](https://www.npmjs.com/package/@totemorg/enums)
@requires [socketio](https://www.npmjs.com/package/@totemorg/socketio)
@requires [socket.io](https://www.npmjs.com/package/socket.io)
@requires [crypto](https://nodejs.org/docs/latest/api/)
@requires [cp](https://nodejs.org/docs/latest/api/)
@example
On the server:
import {configLink,notifyClients} from "securelink";
configLink({
server: server,
guest: {....}
});
notifyClients( "update", { // send update request
});
On the client:
// <script src="securelink-client.js"></script>
Sockets({ // establish sockets
update: req => { // intercept update request
console.log("update", req);
},
// other sockets as needed ...
});
*/
const
{
LINK_PASS,
LINK_HOST
} = process.env;
// globals
import CRYPTO from "crypto";
import CP from "child_process";
// For legacy buggy socket.io
//SOCKETIO = require('socket.io'), // Socket.io client mesh
//SIOHUB = require('socket.io-clusterhub'); // Socket.io client mesh for multicore app
//HUBIO = new (SIOHUB);
// For working socketio
import SOCKETIO from "@totemorg/socketio";
import { Copy, Each, Start, Log, sqlThread, neoThread, Notify } from "@totemorg/core";
var
SIO = null;
const
minPassLen = 4,
passwordLen = 16,
//accountLen: 16,
//sessionLen: 32,
tokenLen = 32,
expireTemp = [5,10],
expirePerm = [365,0],
expirePass = [1,0],
TRACE = (msg, ...args) => `secure>>>${msg}`.trace( args ),
ERR = err => new Error("securelink//"+err),
SQL = {
logVisit: "INSERT INTO openv.visits SET ?",
setProfile: "UPDATE openv.profiles SET ? WHERE ?",
//setOnline: "UPDATE openv.profiles SET online=1 WHERE Client=?",
//addProfile: "INSERT INTO openv.profiles SET ?",
getProfile: "SELECT * FROM openv.profiles WHERE Client=? LIMIT 1",
//getSession: "SELECT * FROM openv.profiles WHERE SessionID=? LIMIT 1",
addSession: "INSERT INTO openv.sessions SET ? ON DUPLICATE KEY UPDATE Count=Count+1,?",
endSession: "SELECT max(timestampdiff(minute,Opened,now())) AS T, count(ID) AS N FROM openv.sessions WHERE Client=?",
getRiddle: "SELECT * FROM openv.riddles WHERE ? LIMIT 1",
getAccount: "SELECT *,aes_decrypt(unhex(Password),?) AS Password, hex(aes_encrypt(ID,?)) AS SessionID FROM openv.profiles WHERE Client=?",
addAccount: "INSERT INTO openv.profiles SET ?, Password=hex(aes_encrypt(?,?))",
//setPassword: "UPDATE openv.profiles SET Password=hex(aes_encrypt(?,?)), LinkToken=? WHERE Client=?",
setPassword: "UPDATE openv.profiles SET Expires=null, Password=hex(aes_encrypt(?,?)) WHERE least(?,1)",
getToken: "SELECT Client FROM openv.profiles WHERE TokenID=? AND Expires>now()",
addToken: "UPDATE openv.profiles SET TokenID=? WHERE Client=?"
//addSession: "UPDATE openv.profiles SET SessionID=? WHERE Client=?",
//endSession: "UPDATE openv.profiles SET SessionID=null WHERE Client=?",
},
ERROR = {
/**
* */
userOnline: ERR( "account already online" ),
/**
* */
userExpired: ERR( "account expired" ),
/**
* */
badReset: ERR("password reset failed"),
/**
* */
badPass: ERR("password not complex enough"),
/**
* */
badLogin: ERR("login failed"),
/**
* */
badOptions: ERR( "bad/missing options" ),
//resetOk: ERR("password reset"),
//loginBlocked: ERR("account blocked"),
//noGuests: ERR("guests blocked"),
//nonGuest: ERR("must be guest"),
//userPending: ERR("account verification pending -- see email")
};
/**
Start a secure link and return the user profile corresponding for the supplied
account/password login. The provided callback LOGIN(err,profile) where LOGIN =
resetPassword || newAccount || newSession || guestSession determines the login session
type being requested.
@cfg {Function}
@param {String} account credentials
@param {String} password credentials
@param {Function} cb callback (err,profile) to process the session
*/
export function Login (account,password,cb) {
function passwordOk( pass ) {
return (pass.length >= minPassLen);
}
function accountOk( acct ) {
const
[account,domain] = acct.split("@");
return bannedDomains[domain] ? false : true;
}
function getExpires( expire ) {
const
{ round, random } = Math,
[min,max] = expire,
expires = new Date();
expires.setDate( expires.getDate() + min + round(random()*max) );
return expires;
}
function genToken( cb ) {
function genCode( len, cb ) {
return CRYPTO.randomBytes( len/2, (err, code) => cb( code.toString("hex") ) );
}
genCode(tokenLen, token => cb(token, getExpires(expirePass)) );
}
function newAccount( sql, account, password, cb) {
const
{ guest } = SECLINK;
//Log("new acct guest", guest);
if ( guest ) // guest profiles allowed
if (addAccount)
if ( accountOk(account) )
genToken( (token,expires) => {
//Log("token", token,expires);
const
trusted = isTrusted( account ),
prof = Copy({
LinkToken: token, // secure validation token
Trusted: trusted, // enable to allow client to send encrypted messages
Challenge: !trusted, // enable to challenge user at session join
Client: account, // client name/email
Expires: expires // getExpires( trust ? expireTemp : expirePerm )
}, Copy( guest, {} ));
sql.query( addAccount, [
prof,
password,
LINK_PASS
], (err,info) => {
cb( (err || !info.insertId) ? null : prof );
});
});
else
cb(null);
else
cb( null );
else {
//TRACE("No account template");
cb( null );
}
}
function getProfile( sql, account, cb ) {
//TRACE("get profile", account);
sql.query( getAccount, [LINK_PASS, LINK_PASS, account], (err,profs) => {
cb( err ? null : profs[0] || null );
});
}
const
{ getAccount, addAccount, addToken, getToken, setPassword } = SQL;
TRACE(cb.name, `${account}/${password}`);
sqlThread( sql => {
switch ( cb.name ) {
case "resetPassword": // host requesting a password reset
getProfile( sql, account, prof => {
TRACE("login resetPass", [password, prof]);
if ( prof ) { // have a valid user login
const
{ LinkToken,Expires } = prof,
chgreq = "password change request".tag("a", { href: "/link".tag("?", { login:account, token:LinkToken}) });
Notify({
to: account,
subject: "Totem account verification",
html: `Your ${chgreq} expires ${Expires}`
});
cb( null, prof );
/*
sql.query( setPassword, [password, LINK_PASS, secureToken, account], err => {
TRACE("setpass", [account, password]);
cb( err ? ERROR.badReset : null, prof );
}); */
/*genToken( sql, account, (token,expires) => { // gen a token account
cb( ERROR.userPending );
Notify({
to: account,
subject: "Totem password reset request",
text: `Please login using ${token}/NEWPASSWORD by ${expires}`
});
}); */
}
else
cb( ERROR.badLogin );
});
break;
case "newAccount": // host requesting a new account
//Log("login newAccount", [account, password]) ;
newAccount( sql, account, password, prof => {
if ( prof ) {
const
{ LinkToken, Expires } = prof,
valid = "login validation".tag("a", { href: "/link".tag("?", {login:account, token:LinkToken}) });
//Log("new acct", LinkToken, Expires);
Notify({
to: account,
subject: "Totem account verification",
html: `Your ${valid} expires ${Expires}`
});
cb( null, prof );
}
else
cb( ERROR.badLogin );
});
break;
case "newSession": // host requesting an authorized session
case "loginSession":
case "authSession":
getProfile( sql, account, prof => {
if ( prof ) { // have a valid user login
TRACE("login newSession", [password, prof.Password]);
if ( prof.Banned ) // account banned for some reason
cb( ERR(prof.Banned) );
/*
else
if ( prof.Online ) // account already online
cb( ERROR.userOnline );
*/
/*
else
if ( prof.Expires ? prof.Expires < new Date() : false )
cb( ERROR.userExpired );
*/
/*
else
if ( prof.TokenID ) // password reset pending
if ( passwordOk(password) )
sql.query( setPassword, [password, LINK_PASS, allowSecureConnect, account], err => {
TRACE("setpass", account, password);
cb( err ? ERROR.badReset : ERROR.resetOk );
});
else
cb( ERROR.badPass );
*/
else
if (password == prof.Password) // login validated
cb(null, prof);
/*genSession( sql, account, (sessionID,expires) => cb(null, Copy({
sessionID: sessionID,
expires: expires
}, prof ) ));*/
else
cb( ERROR.badLogin );
}
else
cb( ERROR.badLogin );
});
break;
case "guestSession": // host requesting a guest session
case "noauthSession":
default:
getProfile( sql, account, prof => {
//Log("guestSession Login prof", prof);
if ( prof )
if ( prof.Banned )
cb( ERR(prof.Banned) );
else
cb( null, prof );
else
newAccount( sql, account, "", prof => {
//Log("new guest", prof);
if ( prof )
cb( null, prof );
else
cb( ERROR.badLogin );
});
/*
else // foreign account
sql.query( getToken, [account], (err,profs) => { // try to locate by tokenID
if ( prof = profs[0] )
cb( null, prof );
else // try to locate by sessionID
sql.query( getSession, [account], (err,profs) => { // try to locate by sessionID
cb( err, err ? null : profs[0] || null );
});
}); */
});
}
});
}
/**
Establish socketio channels for the SecureIntercom link (at store,restore,login,relay,status,
sync,join,exit,content) and the insecure dbSync link (at select,update,insert,delete).
*/
export function configLink ( opts ) {
function extendChallenger ( ) { //< Create antibot challenges.
const
{ store, extend, map, captchaEndpoint } = challenge,
{ floor, random } = Math;
TRACE( `Using ${extend} challenges from ${captchaEndpoint}` );
if ( captchaEndpoint )
for (var n=0; n<extend; n++) {
var
Q = {
x: floor(random()*10),
y: floor(random()*10),
z: floor(random()*10),
n: floor(random()*map["0"].length)
},
A = {
x: "".tag("img", {src: `${captchaEndpoint}/${Q.x}/${map[Q.x][Q.n]}.jpg`}),
y: "".tag("img", {src: `${captchaEndpoint}/${Q.y}/${map[Q.y][Q.n]}.jpg`}),
z: "".tag("img", {src: `${captchaEndpoint}/${Q.z}/${map[Q.z][Q.n]}.jpg`})
};
store.push( {
Q: `${A.x} * ${A.y} + ${A.z}`,
A: Q.x * Q.y + Q.z
} );
}
//TRACE(JSON.stringify(store));
}
const
{ inspect, guest, server, challenge } = Copy( opts, SECLINK, "." ),
{ getProfile, addSession } = SQL;
SIO = SOCKETIO(server);
/*{ // socket.io defaults but can override using
serveClient: true, // default true to prevent server from intercepting path
path: "/socket.io" // default get-url that the client-side connect issues on calling io()
}), */
TRACE( guest
? "Guest logins enabled"
: "Guest logins disabled" );
if (guest) { // prepare default guest record for clone
delete guest.ID;
delete guest.LinkToken;
delete guest.Password;
}
SIO.on("connect", socket => { // define socket listeners when client calls the socketio-client io()
TRACE("listening to sockets");
socket.on("join", (req,socket) => { // join this client with other clients
const
{client,message,police,location,ip,agent,platform,room} = req;
TRACE("join",{client,room,police});
sqlThread( sql => {
if ( addSession ) { // log sessions if allowed
const log = { // collect browser supplied info
Opened: new Date(),
Client: client,
Location: location,
IP: ip,
Agent: agent,
Platform: platform
};
sql.query( addSession, [log,log] );
}
sql.query(getProfile, [client], (err,profs) => {
/*
Create an antibot challenge and relay to client with specified profile parameters
@param {String} client being challenged
@param {Object} profile with a .Message riddle mask
*/
function getChallenge (profile, cb) {
/**
Check clients response req.query to a antibot challenge.
@param {String} msg riddle mask contianing (riddle), (yesno), (ids), (rand), (card), (bio) keys
@param {Array} rid List of riddles returned
@param {Object} ids Hash of {id: value, ...} replaced by (ids) key
*/
function makeChallenge () {
const
{ floor, random } = Math,
rand = N => floor( random() * N ),
N = store.length,
randRiddle = () => store[rand(N)];
return N ? randRiddle() : {Q:"1+0", A:"1"};
}
const
{ checkEndpoint, store } = challenge,
{ Message, Retries, Timeout } = profile,
{ Q,A } = makeChallenge( );
///TRACE("genriddle", client, [Q,A]);
sql.query("REPLACE INTO openv.riddles SET ?", { // track riddle
Riddle: A,
Client: client,
Made: new Date(),
Attempts: 0,
maxAttempts: Retries
}, (err,info) => cb({ // send challenge to client
message: "??"+((Message||"What is: ").parse$(profile))+Q,
retries: Retries || 5,
timeout: Timeout || 30,
callback: checkEndpoint,
//passphrase: prof.LinkToken || ""
}) );
}
/*
Get public keys for all clients in specified room
*/
function getMembers( room, cb ) {
const
keys = {};
//TRACE("========================", room);
sql.query("SELECT Client,pubKey FROM openv.profiles WHERE Room=?", room )
.on("result", rec => keys[rec.Client] = rec.pubKey )
.on("end", () => cb( keys ) );
}
//TRACE(err,profs);
const
prof = profs[0];
//Log(prof);
if ( prof ) {
const
{ Banned, LinkToken, Challenge } = prof;
if ( Banned )
SIO.clients[client].emit("status", { // Notify client they are banned
message: `${client} banned: ${Banned}`
});
else
if ( LinkToken ) // allowed to use secure link
if ( Challenge ) // must solve challenge to use secure link
getChallenge( prof, riddle => { // get a riddle for this challenge
TRACE("challenge", riddle);
//socket.emit("challenge", riddle);
getMembers( room, pubKeys => { // get public keys from all online clients
riddle.pubKeys = pubKeys;
riddle.passphrase = LinkToken;
TRACE("start riddle");
SIO.clients[client].emit("start", riddle);
});
});
else
getMembers( room, pubKeys => { // get public keys of clients in this room
SIO.clients[client].emit("start", { // start secure link with passphrase of client's link token
message: `Welcome ${client}`,
from: "secureLink",
passphrase: LinkToken,
pubKeys: pubKeys
});
});
else
getMembers( room, pubKeys => { // get public keys of clients in this room
SIO.clients[client].emit("start", { // start insecure link with no passphrase
message: `Welcome ${client}`,
from: "secureLink",
passphrase: "",
pubKeys: pubKeys
});
});
}
else
SIO.clients[client].emit("status", {
message: `Cant find ${client}`
});
try {
}
catch (err) {
TRACE(err,"join failed");
}
});
});
});
socket.on("store", (req,socket) => { // store client's message history
const
{client,ip,location,message} = req;
TRACE("store client history");
sqlThread( sql => {
sql.query(
"INSERT INTO openv.saves SET ? ON DUPLICATE KEY UPDATE Content=?",
[{Client: client,Content:message}, message],
err => {
try {
SIO.clients[client].emit("status", {
message: err ? "failed to store history" : "history stored"
});
}
catch (err) {
TRACE(err,"History load failed");
}
});
});
});
socket.on("restore", (req,socket) => { // restore client's message history
const
{client,ip,location,message} = req;
TRACE("restore client history");
sqlThread( sql => {
sql.query("SELECT Content FROM openv.saves WHERE Client=? LIMIT 1",
[client],
(err,recs) => {
//TRACE("restore",err,recs);
try {
if ( rec = err ? null : recs[0] )
SIO.clients[client].emit("content", {
message: rec.Content
});
else
SIO.clients[client].emit("status", {
message: "cant restore history"
});
}
catch (err) {
TRACE(err,"History restore failed");
}
});
});
});
socket.on("login", (req,socket) => { // login/logoff/reset client
const
{ login, client, user, pass } = req,
{ setOnline, setPassword } = SQL;
TRACE("login", {client,user,pass} );
try {
if ( user )
switch ( pass || "" ) {
/*
case "reset":
Login( password, function resetPassword(status) {
TRACE("socket resetPassword", status);
SIO.clients[password].emit("status", {
message: status,
});
});
break;
*/
case "logout":
sqlThread( sql => sql.query("UPDATE openv.profiles SET online=0 WHERE Client=?", client) );
SIO.emit("remove", { // broadcast client's pubKey to everyone
client: client
});
break;
case "reset":
Login( user, pass, function resetPassword(err,prof) {
TRACE("resetPassword", err, prof);
SIO.clients[client].emit("status", {
message: err ? "Password reset failed" : "Reset verification sent"
});
});
break;
case "new":
Login( user, pass, function newAccount(err,prof) {
TRACE("newAccount", err, prof);
if ( err )
SIO.clients[client].emit("status", { // return error msg to client
message: "Account creation failed"
});
else
SIO.clients[client].emit("status", { // return login ok msg to client with convenience cookie
message: "Account verification sent"
//cookie: `session=${prof.Client}; expires=${prof.Expires.toUTCString()}; path=/`
//passphrase: prof.LinkToken // nonnull if account allowed to use secureLink
});
});
break;
case "":
SIO.clients[client].emit("status", { // return error msg to client
message: "Login missing password"
});
break;
default:
Login( user, pass, function newSession(err,prof) {
TRACE("newSession", err, prof);
if ( err )
SIO.clients[client].emit("status", { // return error msg to client
message: "Login failed"
});
else {
if ( setOnline )
sqlThread( sql => sql.query(setOnline, user) );
SIO.clients[client].emit("status", { // return login ok msg to client with convenience cookie
message: "Login completed",
cookie: `session=${prof.Client}; expires=${prof.Expires}; path=/`
//passphrase: prof.LinkToken // nonnull if account allowed to use secureLink
});
SIO.emit("remove", { // Notify all clients to remove this client
client: client
});
SIO.emit("accept", { // Notify all clients to accept this client
client: user,
pubKey: prof.pubKey
});
}
});
}
else
if ( setPassword )
sqlThread( sql => {
sql.query(
setPassword, [
pass,
LINK_PASS,
{Client:client}
],
(err,info) => {
//Log(err,info);
SIO.clients[client].emit("status", {
message: err ? "Password reset failed" : "Password reset"
});
});
});
else
SIO.clients[client].emit("status", { // return error msg to client
message: "Login invalid"
});
}
catch (err) {
throw err;
}
});
socket.on("relay", (req,socket) => { // relay message to another client
const
{ from,message,to,police,route } = req;
TRACE("relay client message", req);
if ( message.indexOf("PGP PGP MESSAGE")>=0 ) // just relay encrypted messages
SIO.emitOthers(from, "relay", { // broadcast message to everyone
message: message,
from: from,
to: to
});
else
if ( police ) // relay scored messages that are unencrypted
inspect( message, to, score => {
sqlThread( sql => {
sql.query( endSession, [from], (err,recs) => {
const
{N,T} = err ? {N:0,T:1} : recs[0],
lambda = N/T;
//TRACE("inspection", score, lambda, hops);
if ( police ) // if tracking permitted by client then ...
sql.query(
"INSERT INTO openv.relays SET ?", {
Message: message,
Rx: new Date(),
From: from,
To: to,
New: 1,
Score: JSON.stringify(score)
} );
SIO.emitOthers(from, "relay", { // broadcast message to everyone
message: message,
score: Copy(score, {
Activity:lambda,
Hopping:0
}),
from: from,
to: to
});
});
});
});
else // relay message as-is
SIO.emitOthers(from, "relay", { // broadcast message to everyone
message: message,
from: from,
to: to
});
});
socket.on("announce", req => { // announce arrival to other clients
TRACE("announce client");
const
{ client,pubKey } = req;
sqlThread( sql => {
sql.query(
"UPDATE openv.profiles SET pubKey=?,Online=1 WHERE Client=?",
[pubKey,client] );
if (0)
sql.query( "SELECT Client,pubKey FROM openv.profiles WHERE Client!=? AND length(pubKey)", [client] )
.on("result", rec => {
TRACE("send sync to me");
socket.emit("sync", { // broadcast other pubKeys to this client
message: rec.pubKey,
from: rec.Client,
to: client
});
});
});
SIO.emit("accept", { // broadcast client's pubKey to everyone
pubKey: pubKey,
client: client,
});
});
socket.on("kill", (req,socket) => { // kill session
TRACE("kill client session");
socket.end();
});
});
/*
// for debugging
SIO.on("connect_error", err => {
TRACE(err);
});
SIO.on("disconnection", socket => {
TRACE(">>DISCONNECT CLIENT");
});
*/
extendChallenger ( );
return SECLINK;
}
/**
*/
export function notifyClients (channel, msg) {
if (SIO) SIO.emit(channel,msg);
}
/**
Reset account request with callback cb( err ).
*/
function resetClient(client, token, cb) {
const
{setPassword} = SQL;
function genToken( cb ) {
function genCode( len, cb ) {
return CRYPTO.randomBytes( len/2, (err, code) => cb( code.toString("hex") ) );
}
genCode( passwordLen, cb );
}
if ( setPassword )
genToken( pass => {
TRACE(setPassword, pass, token);
sqlThread( sql => {
sql.query(
setPassword, [
pass,
LINK_PASS,
{Client:client, LinkToken:token}
],
(err,info) => {
//Log(err,info);
cb( (err || !info.affectedRows) ? ERR(ERROR.badReset) : null );
});
});
});
else
cb( null );
}
/**
Test response of client during a session challenge.
@param {String} client name of client being challenged
@param {String} guess guess provided by client
@param {Function} res response callback( "pass" || "fail" || "retry" )
*/
function testClient(client,guess,res) {
const
{ getRiddle }= SQL;
if ( getRiddle )
sqlThread( sql => {
sql.query(getRiddle, {Client:client}, (err,recs) => {
let rec = recs[0];
if ( rec ) {
const
ID = {ID:rec.ID},
Guess = (guess+"").replace(/ /g,"");
TRACE("riddle",rec);
if (rec.Riddle == Guess) {
res( "pass" );
sql.query("DELETE FROM openv.riddles WHERE ?",ID);
}
else
if (rec.Attempts > rec.maxAttempts) {
res( "fail" );
sql.query("DELETE FROM openv.riddles WHERE ?",ID);
}
else {
res( "retry" );
sql.query("UPDATE openv.riddles SET Attempts=Attempts+1 WHERE ?",ID);
}
}
else
res( "fail" );
});
});
else
res( "pass" );
}
export const
bannedDomains = {
"tempail.com": 1,
"temp-mail.io":1,
"anonymmail.net":1,
"mail.tm": 1,
"tempmail.ninja":1,
"getnada.com":1,
"protonmail.com":1,
"maildrop.cc":1,
"":1,
//"guest": 1
};
const SECLINK = {
//sqlThread: () => { throw ERR("sqlThread not configured"); },
server: null, // established on config - disabled by default
guest: null, // guest profile - disabled by default
challenge: { //< for antibot client challenger
extend: 0, // number to add to store
store: [], // challenge store
checkEndpoint: "Undefined checkEndpoint", // endpoint to test client response
captchaEndpoint: "Undefined captchaEndpoint", // endpoint to provide images
map: []
},
inspect: (doc,to,cb) => {
// throw ERR("securelink inspect not configured");
},
}
/**
Test if an account is "trusted" to use the secure com channel.
*/
function isTrusted(account) { return !account.startsWith("guest@"); }
export const agents = {
/**
Endpoint to validate clients response to an antibot challenge, login client, set client focus,
invite/accep/enter a client to a room, or promote client to a user.
@param {requestHash} req session request
@param {responseCallback} res session response
*/
link: (req,res) => {
const
{ query, sql, type, body, action, client, profile } = req,
{ login , guess, token, invite, accept, detach, focus, enter, promote } = (action=="select") ? query : body,
{ Login } = profile,
{ setProfile, logVisit } = SQL;
Log( "/link", {client,token,guess,focus,invite,accept,login,enter,promote,login,guess,token});
neoThread( neo => {
if ( focus )
sql.query( setProfile, [{Focus: focus}, {Client: client}], err => res(err||"ok") );
else
if (invite) {
res("ok");
invite.split(",").forEach( user => neo.invite( client, user ) );
}
else
if (accept) {
res("ok");
accept.split(",").forEach( user => neo.accept( client, user ) );
}
else
if (promote) {
if ( promote.indexOf("@guest") >= 0 )
res( "A guest cant be promoted to User" );
else
if (Login)
res( "Already promoted" );
else {
const
login = "junk";
sql.query( setProfile, {Login: login}, err => Log("set login", err));
CP.exec( `mkdir ./config/users/${login}`, err => {
Log("mkdir", err);
CP.exec( `cp -r ./config/users/template/* ./config/users/${login}`, err => Log("cp", err) );
});
}
}
else
if (login)
if (guess)
testClient(login,guess,res);
else
if (token)
resetClient( login, token, res( err ? null : "password reset accepted" ));
else
res( ERROR.badLogin );
else
if (enter) {
const
//pairs = {},
me = "brian james",
scanRoom = enter.replace("site","");
sql.query( logVisit, { Entered: new Date(), Client: client, Room: enter });
sql.query( setProfile, [{Room: enter}, {Client: client}], err => {
sql.getClients( scanRoom, clients => {
//Log("get==============", clients, scanRoom);
neo.getNetwork( "users", me, (err, {links}) => { // get network attached to me
//net.forEach( pair => pairs[pair.a.name+"->"+pair.b.name] = pair.r );
//Log( {User, net, clients, pairs} );
//Log( JSON.stringify(net));
//Log( JSON.stringify(links) );
if (err)
res( err );
else
res( clients.map( client => {
client.icons = ["avatar"].map( png => "".tag("img",{src:`/config/users/${client.Login}/icons/${png}.png`,width:20,height:20}) ).join("");
if (client.Login != "guest") {
client.links = "Contact".link( "mailto:"+client.Client );
client.links += "|"+"Focus".link( "/link".tag("?", {focus: client.Login }));
client.links += "|"+"Meet".link( `/${client.Room}.view` );
links.forEach( link => {
switch ( link.name ) {
case "invite":
client.links += "|"+"Accept".link( "/link".tag("?", {accept: client.Name }));
break;
case "accept":
client.links += "|"+"Retract".link( "/link".tag("?", {retract: client.Name }));
break;
default:
client.links += "|"+"Invite".link( "/link".tag("?", {invite: client.Name }));
}
});
}
return client;
}) );
});
});
});
}
else
res( ERROR.badOptions );
});
},
}
Start("securelink", {
"env": {
LINK_PASS
}
});
// UNCLASSIFIED