Merge branch 'google-login' into 'dev'

Google Sign-In Button and Login Mechanism Partial Rewrite

See merge request Shinobi-Systems/Shinobi!297
auto-build-api-doc-with-code
Moe 2021-04-04 18:40:11 +00:00
commit cf588837dd
24 changed files with 1253 additions and 497 deletions

View File

@ -85,6 +85,8 @@ require('./libs/ffmpeg.js')(s,config,lang, async () => {
require('./libs/scheduler.js')(s,config,lang,app,io)
//onvif device manager
require('./libs/onvifDeviceManager.js')(s,config,lang,app,io)
//alternate logins
require('./libs/auth/logins.js')(s,config,lang,app)
//on-start actions, daemon(s) starter
await require('./libs/startup.js')(s,config,lang)
//p2p, commander

View File

@ -4215,6 +4215,17 @@ module.exports = function(s,config,lang){
},
]
},
"AlternateLogins": {
"name": lang["Alternate Logins"],
"color": "orange",
"info": [
{
"form-group-class-pre-layer": "form-group",
"fieldType": 'div',
"id": "alternate-logins"
},
]
},
"2-Factor Authentication": {
"name": lang['2-Factor Authentication'],
"color": "grey",

View File

@ -113,7 +113,18 @@
"Never": "Never",
"API": "API",
"ONVIF": "ONVIF",
"Alternate Logins": "Alternate Logins",
"Unlink": "Unlink",
"Unlinked": "Unlinked",
"Unlink Login": "Unlink Login?",
"lastLogin": "Last Login",
"Home": "Home",
"alreadyLinked": "Already Linked to an Account",
"Link Google Account": "Link Google Account",
"noLoginTokensAdded": "There are no Alternate Logins associated to this account.",
"tokenNotUserBound": "This Login Handle is not linked to a user on this server!",
"tokenNotUserBoundPt2": "Type your credentials then use the Google Sign-In button to link quickly.",
"loginHandleUnbound": "Login has been unlinked from this account.",
"Set Home": "Set Home",
"Set Home Position (ONVIF-only)": "Set Home Position (ONVIF-only)",
"Non-Standard ONVIF": "Non-Standard ONVIF",

View File

@ -5,6 +5,7 @@ module.exports = function(s,config,lang){
s.superUsersApi = {}
s.factorAuth = {}
s.failedLoginAttempts = {}
s.alternateLogins = {}
//
var getUserByUid = function(params,columns,callback){
if(!columns)columns = '*'
@ -247,39 +248,18 @@ module.exports = function(s,config,lang){
if(userSelected){
resp.$user = userSelected
}
if(adminUsersSelected){
resp.users = adminUsersSelected
}
}
callback({
ip : ip,
$user: userSelected,
users: adminUsersSelected,
config: chosenConfig,
lang: lang
})
}
var foundUser = function(){
if(params.users === true){
s.knexQuery({
action: "select",
columns: "*",
table: "Users",
where: [
['details','NOT LIKE','%"sub"%'],
]
},(err,r) => {
adminUsersSelected = r
success()
})
}else{
success()
}
}
if(params.auth && Object.keys(s.superUsersApi).indexOf(params.auth) > -1){
userFound = true
userSelected = s.superUsersApi[params.auth].$user
foundUser()
success()
}else{
var superUserList = JSON.parse(fs.readFileSync(s.location.super))
superUserList.forEach(function(superUser,n){
@ -300,7 +280,7 @@ module.exports = function(s,config,lang){
){
userFound = true
userSelected = superUser
foundUser()
success()
}
})
}

View File

@ -0,0 +1,87 @@
module.exports = (s,config,lang) => {
async function getLoginToken(loginId,bindType) {
bindType = bindType ? bindType : 'google'
return (await s.knexQueryPromise({
action: "select",
columns: '*',
table: "LoginTokens",
where: [
['loginId','=',`${bindType}-${loginId}`],
['type','=',bindType],
],
limit: 1
})).rows[0]
}
async function bindLoginIdToUser(options) {
const response = {ok: false}
const loginId = options.loginId
const groupKey = options.ke
const userId = options.uid
const name = options.name
const bindType = options.type ? options.type : 'google'
const searchResponse = await s.knexQueryPromise({
action: "select",
columns: '*',
table: "LoginTokens",
where: [
['loginId','=',`${bindType}-${loginId}`],
['type','=',bindType],
]
})
if(!searchResponse.rows[0]){
const insertResponse = await s.knexQueryPromise({
action: "insert",
table: "LoginTokens",
insert: {
loginId: `${bindType}-${loginId}`,
type: bindType,
ke: groupKey,
uid: userId,
name: name,
lastLogin: new Date(),
}
})
response.ok = insertResponse.ok
}else{
response.msg = lang.alreadyLinked
}
return response
}
async function refreshLoginTokenAccessDate(loginId,bindType) {
const response = {ok: false}
bindType = bindType ? bindType : 'google'
const updateResponse = await s.knexQueryPromise({
action: "update",
table: "LoginTokens",
update: {
lastLogin: new Date()
},
where: [
['loginId','=',`${bindType}-${loginId}`],
['type','=',bindType],
]
})
response.ok = updateResponse.ok
return response
}
async function deleteLoginToken(loginId) {
const response = {ok: false}
bindType = bindType ? bindType : 'google'
const updateResponse = await s.knexQueryPromise({
action: "delete",
table: "LoginTokens",
where: [
['loginId','=',`${bindType}-${loginId}`],
['type','=',bindType],
]
})
response.ok = updateResponse.ok
return response
}
return {
getLoginToken: getLoginToken,
deleteLoginToken: deleteLoginToken,
bindLoginIdToUser: bindLoginIdToUser,
refreshLoginTokenAccessDate: refreshLoginTokenAccessDate,
}
}

149
libs/auth/google.js Normal file
View File

@ -0,0 +1,149 @@
const {OAuth2Client} = require('google-auth-library');
module.exports = (s,config,lang,app) => {
const {
basicAuth,
} = require('./utils.js')(s,config,lang)
const {
getLoginToken,
deleteLoginToken,
bindLoginIdToUser,
refreshLoginTokenAccessDate,
} = require('./alternateLogins.js')(s,config,lang)
console.error(`Google App ID : ${config.appIdGoogleSignIn}`)
const client = new OAuth2Client(config.appIdGoogleSignIn);
async function verifyToken(userLoginToken) {
const ticket = await client.verifyIdToken({
idToken: userLoginToken,
audience: config.appIdGoogleSignIn,
});
const payload = ticket.getPayload();
const userid = payload['sub'];
return {
ok: !!payload.email,
user: payload.email ? {
id: userid,
name: payload.name,
email: payload.email,
picture: payload.picture,
} : null,
}
}
async function loginWithGoogleAccount(userLoginToken) {
const response = {ok: false, googleSignedIn: false}
const tokenResponse = await verifyToken(userLoginToken)
if(tokenResponse.ok){
const user = tokenResponse.user
response.googleSignedIn = true
response.googleUser = user
const foundToken = await getLoginToken(user.id,'google')
if(foundToken){
const userResponse = await s.knexQueryPromise({
action: "select",
columns: '*',
table: "Users",
where: [
['uid','=',foundToken.uid],
['ke','=',foundToken.ke],
],
limit: 1
})
response.ok = true
userResponse.rows[0].details = s.parseJSON(userResponse.rows[0].details)
response.user = userResponse.rows[0]
}else{
response.msg = lang.tokenNotUserBound
if(config.allowBindingAltLoginsFromLoginPage){
response.msg += '\n' + lang.tokenNotUserBoundPt2
}
// make new if no users?
}
}
return response
}
s.onProcessReady(() => {
config.renderPaths.loginTokenAddGoogle = `pages/loginTokenAddGoogle`
s.alternateLogins['google'] = async (params) => {
const response = { ok: false }
const loginToken = params.alternateLoginToken
const username = params.mail
const password = params.pass
const googleLoginResponse = await loginWithGoogleAccount(loginToken)
if(googleLoginResponse.user){
const user = googleLoginResponse.user
response.ok = true
response.user = user
refreshLoginTokenAccessDate(googleLoginResponse.googleUser.id,'google')
}else if(config.allowBindingAltLoginsFromLoginPage && googleLoginResponse.googleSignedIn && username && password){
const basicAuthResponse = await basicAuth(username,password)
if(basicAuthResponse.user){
const user = basicAuthResponse.user
const loginId = googleLoginResponse.googleUser.id
const groupKey = user.ke
const userId = user.uid
const bindResponse = await bindLoginIdToUser({
loginId: loginId,
ke: groupKey,
uid: userId,
name: googleLoginResponse.googleUser.name,
type: 'google'
})
response.ok = true
response.user = basicAuthResponse.user
}
}else{
response.msg = googleLoginResponse.msg
}
return response
}
s.definitions["Account Settings"].blocks["AlternateLogins"].info.push({
"form-group-class-pre-layer": "form-group",
"fieldType": "btn",
"class": `btn-info google-sign-in`,
"btnContent": `<i class="fa fa-google"></i> &nbsp; ${lang['Link Google Account']}`,
})
s.customAutoLoadTree['LibsJs'].push(`dash2.googleSignIn.js`)
})
/**
* API : Add Token Window (Sign-In to Google) (GET)
*/
app.get(config.webPaths.apiPrefix+':auth/loginTokenAddGoogle/:ke', function (req,res){
s.auth(req.params,(user) => {
s.renderPage(req,res,config.renderPaths.loginTokenAddGoogle,{
lang: lang,
config: s.getConfigWithBranding(req.hostname),
$user: user
})
},res,req);
});
/**
* API : Add Token Window (Sign-In to Google) (POST)
*/
app.post(config.webPaths.apiPrefix+':auth/loginTokenAddGoogle/:ke', function (req,res){
const response = {ok: false};
s.auth(req.params,async (user) => {
const userId = user.uid
const groupKey = req.params.ke
const loginToken = req.body.loginToken
const tokenResponse = await verifyToken(loginToken)
if(tokenResponse.ok){
const googleUser = tokenResponse.user
const loginId = googleUser.id
const bindResponse = await bindLoginIdToUser({
loginId: loginId,
ke: groupKey,
uid: userId,
name: googleUser.name,
type: 'google'
})
response.ok = bindResponse.ok
response.msg = bindResponse.msg
}
s.closeJsonResponse(res,response)
},res,req);
});
return {
client: client,
verifyToken: verifyToken,
loginWithGoogleAccount: loginWithGoogleAccount,
}
}

8
libs/auth/logins.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = (s,config,lang,app) => {
s.debugLog('!!!!!!!!!')
s.debugLog('Loading Alternate Login Methods...')
if(config.allowGoogleSignOn){
s.debugLog('Google')
require('./google.js')(s,config,lang,app)
}
}

217
libs/auth/utils.js Normal file
View File

@ -0,0 +1,217 @@
var fs = require('fs');
module.exports = function(s,config,lang){
function basicAuth(username,password){
const response = { ok: false }
return new Promise((resolve,reject) => {
s.knexQuery({
action: "select",
columns: "*",
table: "Users",
where: [
['mail','=',username],
['pass','=',s.createHash(password)],
],
limit: 1
},(err,r) => {
if(!err && r && r[0]){
const user = r[0]
response.ok = true
user.details = s.parseJSON(user.details)
response.user = user
}else{
response.err = err
}
resolve(response)
})
})
}
// async function adminAuth(username,password){
// const response = { ok: false }
// const basicAuthResponse = await basicAuth(username,password)
// const user = basicAuthResponse.user
// if(user && !user.details.sub){
// response.ok = true
// response.user = user
// }
// return response
// }
function superUserAuth(params){
const response = { ok: false }
if(!fs.existsSync(s.location.super)){
response.msg = lang.superAdminText
}else{
const authToken = params.auth
const username = params.mail
const password = params.pass
let userFound = false
let userSelected = false
try{
if(authToken && Object.keys(s.superUsersApi).indexOf(authToken) > -1){
userFound = true
userSelected = s.superUsersApi[authToken].$user
}else{
var superUserList = JSON.parse(fs.readFileSync(s.location.super))
superUserList.forEach(function(superUser,n){
if(
userFound === false &&
(
authToken && superUser.tokens && superUser.tokens[authToken] || //using API key (object)
authToken && superUser.tokens && superUser.tokens.indexOf && superUser.tokens.indexOf(authToken) > -1 || //using API key (array)
(
username && username.toLowerCase() === superUser.mail.toLowerCase() && //email matches
(
password === superUser.pass || //user give it already hashed
superUser.pass === s.createHash(password) || //hash and check it
superUser.pass.toLowerCase() === s.md5(password).toLowerCase() //check if still using md5
)
)
)
){
userFound = true
userSelected = superUser
}
})
}
}catch(err){
s.systemLog('The following error may mean your super.json is not formatted correctly.')
s.systemLog('You can reset it by replacing it with the super.sample.json file.')
console.error(`super.json error`)
console.error(err)
}
if(userFound){
response.ok = true
response.user = userSelected
}else{
response.msg = lang['Not Authorized']
}
}
return response
}
function superLogin(username,password){
return new Promise((resolve,reject) => {
const response = { ok: false }
const authResponse = superUserAuth({
mail: username,
pass: password,
})
if(authResponse.ok){
response.ok = true
response.user = authResponse.user
}else{
response.msg = lang['Not Authorized']
}
resolve(response)
})
}
function createTwoFactorAuth(user,machineId,pageTarget){
const userDetails = user.details
const response = {
ok: true,
hasItEnabled: userDetails.factorAuth === "1",
isAnAcceptedMachineId: false,
goToDashboard: false,
}
if(response.hasItEnabled){
if(!userDetails.acceptedMachines||!(userDetails.acceptedMachines instanceof Object)){
userDetails.acceptedMachines={}
}
if(!userDetails.acceptedMachines[machineId]){
if(!s.factorAuth[user.ke]){s.factorAuth[user.ke]={}}
if(!s.factorAuth[user.ke][user.uid]){
s.factorAuth[user.ke][user.uid] = {
key: s.nid(),
user: user
}
s.onTwoFactorAuthCodeNotificationExtensions.forEach(function(extender){
extender(user)
})
}
const factorAuthObject = s.factorAuth[user.ke][user.uid]
factorAuthObject.function = pageTarget
factorAuthObject.info = {
ok: true,
auth_token: user.auth,
ke: user.ke,
uid: user.uid,
mail: user.mail,
details: user.details
}
clearTimeout(factorAuthObject.expireAuth)
factorAuthObject.expireAuth = setTimeout(function(){
s.deleteFactorAuth(user)
},1000*60*15)
}else{
response.isAnAcceptedMachineId = true
}
}
if(!response.hasItEnabled || response.isAnAcceptedMachineId){
response.goToDashboard = true
}
return response
}
function twoFactorVerification(params){
const response = { ok: false }
const factorAuthKey = (params.factorAuthKey || '00').trim()
console.log(params)
console.log(s.factorAuth[params.ke][params.id])
if(
s.factorAuth[params.ke] &&
s.factorAuth[params.ke][params.id] &&
s.factorAuth[params.ke][params.id].key === factorAuthKey
){
const factorAuthObject = s.factorAuth[params.ke][params.id]
// if(factorAuthObject.key===params.factorAuthKey){
const userDetails = factorAuthObject.info.details
if(params.remember==="1"){
if(!userDetails.acceptedMachines||!(userDetails.acceptedMachines instanceof Object)){
userDetails.acceptedMachines={}
}
if(!userDetails.acceptedMachines[params.machineID]){
userDetails.acceptedMachines[params.machineID]={}
s.knexQuery({
action: "update",
table: "Users",
update: {
details: JSON.stringify(userDetails)
},
where: [
['ke','=',params.ke],
['uid','=',params.id],
]
})
}
}
const pageTarget = factorAuthObject.function
factorAuthObject.info.lang = s.getLanguageFile(userDetails.lang)
response.info = Object.assign(factorAuthObject.info,{})
clearTimeout(factorAuthObject.expireAuth)
s.deleteFactorAuth({
ke: params.ke,
uid: params.id,
})
// }else{
// var info = factorAuthObject.info
// renderPage(config.renderPaths.factorAuth,{$user:{
// ke: info.ke,
// id: info.uid,
// mail: info.mail,
// },lang:req.lang});
// res.end();
// }
response.pageTarget = pageTarget
response.ok = true
}
return response
}
function ldapLogin(username,password){
}
return {
basicAuth: basicAuth,
superUserAuth: superUserAuth,
superLogin: superLogin,
createTwoFactorAuth: createTwoFactorAuth,
twoFactorVerification: twoFactorVerification,
ldapLogin: ldapLogin,
}
}

View File

@ -40,7 +40,7 @@ module.exports = function(s,config){
s.sqlQuery = sqlQuery
s.connectDatabase = connectDatabase
s.sqlQueryBetweenTimesWithPermissions = sqlQueryBetweenTimesWithPermissions
s.preQueries = function(){
s.preQueries = async function(){
var knex = s.databaseEngine
var mySQLtail = ''
if(config.databaseType === 'mysql'){
@ -133,6 +133,31 @@ module.exports = function(s,config){
}
},true)
}
try{
s.databaseEngine.schema.createTable('LoginTokens', table => {
table.string('loginId',255).defaultTo('')
table.string('type',25).defaultTo('')
table.string('ke',50).defaultTo('')
table.string('uid',50).defaultTo('')
table.string('name',50).defaultTo('Unknown')
table.timestamp('lastLogin').defaultTo(s.databaseEngine.fn.now())
}).then(() => {
s.databaseEngine.schema.alterTable('LoginTokens', function(table) {
table.unique('loginId')
}).then(() => {
s.systemLog('Created New Database Table : LoginTokens')
}).catch((err) => {
console.log(err)
})
}).catch((err) => {
if(err && err.code !== 'ER_TABLE_EXISTS_ERROR'){
console.log('error')
console.log(err)
}
})
}catch(err){
console.log(err)
}
delete(s.preQueries)
}
}

View File

@ -19,6 +19,13 @@ module.exports = function(s,config,lang,app,io){
const {
triggerEvent,
} = require('./events/utils.js')(s,config,lang)
const {
basicAuth,
superLogin,
createTwoFactorAuth,
twoFactorVerification,
ldapLogin,
} = require('./auth/utils.js')(s,config,lang)
if(config.productType === 'Pro'){
var LdapAuth = require('ldapauth-fork');
}
@ -160,10 +167,10 @@ module.exports = function(s,config,lang,app,io){
s.checkCorrectPathEnding(config.webPaths.home)+':screen',
s.checkCorrectPathEnding(config.webPaths.admin)+':screen',
s.checkCorrectPathEnding(config.webPaths.super)+':screen',
],function (req,res){
],async function (req,res){
var response = {ok: false};
req.ip = s.getClientIp(req)
var screenChooser = function(screen){
const screenChooser = function(screen){
var search = function(screen){
if(req.url.indexOf(screen) > -1){
return true
@ -198,7 +205,7 @@ module.exports = function(s,config,lang,app,io){
return false
}
//
renderPage = function(focus,data){
const renderPage = function(focus,data){
if(s.failedLoginAttempts[req.body.mail]){
clearTimeout(s.failedLoginAttempts[req.body.mail].timeout)
delete(s.failedLoginAttempts[req.body.mail])
@ -213,22 +220,32 @@ module.exports = function(s,config,lang,app,io){
s.renderPage(req,res,focus,data)
}
}
failedAuthentication = function(board){
const failedAuthentication = function(board,failIdentifier,failMessage){
// brute protector
if(!s.failedLoginAttempts[req.body.mail]){
s.failedLoginAttempts[req.body.mail] = {
if(!failIdentifier){
s.renderPage(req,res,config.renderPaths.index,{
failedLogin: true,
message: failMessage || lang.failedLoginText2,
lang: s.copySystemDefaultLanguage(),
config: s.getConfigWithBranding(req.hostname),
screen: screenChooser(req.params.screen)
})
return
}
if(!s.failedLoginAttempts[failIdentifier]){
s.failedLoginAttempts[failIdentifier] = {
failCount : 0,
ips : {}
}
}
++s.failedLoginAttempts[req.body.mail].failCount
if(!s.failedLoginAttempts[req.body.mail].ips[req.ip]){
s.failedLoginAttempts[req.body.mail].ips[req.ip] = 0
++s.failedLoginAttempts[failIdentifier].failCount
if(!s.failedLoginAttempts[failIdentifier].ips[req.ip]){
s.failedLoginAttempts[failIdentifier].ips[req.ip] = 0
}
++s.failedLoginAttempts[req.body.mail].ips[req.ip]
clearTimeout(s.failedLoginAttempts[req.body.mail].timeout)
s.failedLoginAttempts[req.body.mail].timeout = setTimeout(function(){
delete(s.failedLoginAttempts[req.body.mail])
++s.failedLoginAttempts[failIdentifier].ips[req.ip]
clearTimeout(s.failedLoginAttempts[failIdentifier].timeout)
s.failedLoginAttempts[failIdentifier].timeout = setTimeout(function(){
delete(s.failedLoginAttempts[failIdentifier])
},1000 * 60 * 15)
// check if JSON
if(req.query.json === 'true'){
@ -237,7 +254,7 @@ module.exports = function(s,config,lang,app,io){
}else{
s.renderPage(req,res,config.renderPaths.index,{
failedLogin: true,
message: lang.failedLoginText2,
message: failMessage || lang.failedLoginText2,
lang: s.copySystemDefaultLanguage(),
config: s.getConfigWithBranding(req.hostname),
screen: screenChooser(req.params.screen)
@ -251,7 +268,7 @@ module.exports = function(s,config,lang,app,io){
type: lang['Authentication Failed'],
msg: {
for: board,
mail: req.body.mail,
mail: failIdentifier,
ip: req.ip
}
}
@ -263,7 +280,7 @@ module.exports = function(s,config,lang,app,io){
columns: "ke,uid,details",
table: "Users",
where: [
['mail','=',req.body.mail],
['mail','=',failIdentifier],
]
},(err,r) => {
if(r && r[0]){
@ -278,59 +295,40 @@ module.exports = function(s,config,lang,app,io){
})
}
}
checkRoute = function(r){
switch(req.body.function){
function checkRoute(pageTarget,userInfo){
if(!userInfo.lang){
userInfo.lang = s.getLanguageFile(userInfo.details.lang)
}
switch(pageTarget){
case'cam':
s.knexQuery({
action: "select",
columns: "*",
table: "Monitors",
where: [
['ke','=',r.ke],
['type','=','dashcam'],
]
},(err,rr) => {
response.mons = rr
renderPage(config.renderPaths.dashcam,{
// config: s.getConfigWithBranding(req.hostname),
$user: response,
lang: r.lang,
define: s.getDefinitonFile(r.details.lang),
$user: userInfo,
lang: userInfo.lang,
define: s.getDefinitonFile(userInfo.details.lang),
customAutoLoad: s.customAutoLoadTree
})
})
break;
case'streamer':
s.knexQuery({
action: "select",
columns: "*",
table: "Monitors",
where: [
['ke','=',r.ke],
['type','=','socket'],
]
},(err,rr) => {
response.mons=rr;
renderPage(config.renderPaths.streamer,{
// config: s.getConfigWithBranding(req.hostname),
$user: response,
lang: r.lang,
define: s.getDefinitonFile(r.details.lang),
$user: userInfo,
lang: userInfo.lang,
define: s.getDefinitonFile(userInfo.details.lang),
customAutoLoad: s.customAutoLoadTree
})
})
break;
case'admin':
default:
var chosenRender = 'home'
if(r.details.sub && r.details.landing_page && r.details.landing_page !== '' && config.renderPaths[r.details.landing_page]){
chosenRender = r.details.landing_page
if(userInfo.details.sub && userInfo.details.landing_page && userInfo.details.landing_page !== '' && config.renderPaths[userInfo.details.landing_page]){
chosenRender = userInfo.details.landing_page
}
renderPage(config.renderPaths[chosenRender],{
$user: response,
$user: userInfo,
config: s.getConfigWithBranding(req.hostname),
lang:r.lang,
define:s.getDefinitonFile(r.details.lang),
lang: userInfo.lang,
define: s.getDefinitonFile(userInfo.details.lang),
addStorage: s.dir.addStorage,
fs: fs,
__dirname: s.mainDirectory,
@ -338,109 +336,116 @@ module.exports = function(s,config,lang,app,io){
})
break;
}
s.userLog({ke:r.ke,mid:'$USER'},{type:r.lang['New Authentication Token'],msg:{for:req.body.function,mail:r.mail,id:r.uid,ip:req.ip}})
// res.end();
s.userLog({
ke: userInfo.ke,
mid: '$USER'
},{
type: userInfo.lang['New Authentication Token'],
msg: {
for: pageTarget,
mail: userInfo.mail,
id: userInfo.uid,
ip: req.ip
}
if(req.body.mail&&req.body.pass){
req.default=function(){
s.knexQuery({
action: "select",
columns: "*",
table: "Users",
where: [
['mail','=',req.body.mail],
['pass','=',s.createHash(req.body.pass)],
],
limit: 1
},(err,r) => {
if(!err && r && r[0]){
r=r[0];r.auth=s.md5(s.gid());
})
}
if(req.body.alternateLogin && s.alternateLogins[req.body.alternateLogin]){
const alternateLogin = s.alternateLogins[req.body.alternateLogin]
const alternateLoginResponse = await alternateLogin(req.body)
if(alternateLoginResponse.ok && alternateLoginResponse.user){
const user = alternateLoginResponse.user
const sessionKey = s.md5(s.gid())
user.auth = sessionKey
s.knexQuery({
action: "update",
table: "Users",
update: {
auth: r.auth
auth: sessionKey
},
where: [
['ke','=',r.ke],
['uid','=',r.uid],
['ke','=',user.ke],
['uid','=',user.uid],
]
})
response = {
checkRoute(req.body.function,{
ok: true,
auth_token: r.auth,
ke: r.ke,
uid: r.uid,
mail: r.mail,
details: r.details
};
r.details = JSON.parse(r.details);
r.lang = s.getLanguageFile(r.details.lang)
const factorAuth = function(cb){
req.params.auth = r.auth
req.params.ke = r.ke
if(r.details.factorAuth === "1"){
if(!r.details.acceptedMachines||!(r.details.acceptedMachines instanceof Object)){
r.details.acceptedMachines={}
}
if(!r.details.acceptedMachines[req.body.machineID]){
req.complete=function(){
s.factorAuth[r.ke][r.uid].function = req.body.function
s.factorAuth[r.ke][r.uid].info = response
clearTimeout(s.factorAuth[r.ke][r.uid].expireAuth)
s.factorAuth[r.ke][r.uid].expireAuth = setTimeout(function(){
s.deleteFactorAuth(r)
},1000*60*15)
renderPage(config.renderPaths.factorAuth,{$user:{
ke:r.ke,
uid:r.uid,
mail:r.mail
},lang:r.lang})
}
if(!s.factorAuth[r.ke]){s.factorAuth[r.ke]={}}
if(!s.factorAuth[r.ke][r.uid]){
s.factorAuth[r.ke][r.uid]={key:s.nid(),user:r}
s.onTwoFactorAuthCodeNotificationExtensions.forEach(function(extender){
extender(r)
auth_token: user.auth,
ke: user.ke,
uid: user.uid,
mail: user.mail,
details: user.details
})
req.complete()
}else{
req.complete()
return failedAuthentication(req.body.function,req.body.mail,alternateLoginResponse.msg)
}
}else{
checkRoute(r)
}
}else{
checkRoute(r)
}
}
if(r.details.sub){
}else if(req.body.mail && req.body.pass){
async function regularLogin(){
const basicAuthResponse = await basicAuth(req.body.mail,req.body.pass)
if(basicAuthResponse.user){
const user = basicAuthResponse.user;
const sessionKey = s.md5(s.gid())
user.auth = sessionKey
user.lang = s.getLanguageFile(user.details.lang)
s.knexQuery({
action: "update",
table: "Users",
update: {
auth: user.auth
},
where: [
['ke','=',user.ke],
['uid','=',user.uid],
]
})
if(user.details.sub){
const adminUserCheckResponse = await s.knexQueryPromise({
action: "select",
columns: "details",
table: "Users",
where: [
['ke','=',r.ke],
['ke','=',user.ke],
['details','NOT LIKE','%"sub"%'],
],
},function(err,rr) {
if(rr && rr[0]){
rr=rr[0];
rr.details = JSON.parse(rr.details);
r.details.mon_groups = rr.details.mon_groups;
response.details = JSON.stringify(r.details);
factorAuth()
limit: 1,
})
if(adminUserCheckResponse.rows && adminUserCheckResponse.rows[0]){
const adminUser = adminUserCheckResponse.rows[0];
const adminUserDetails = s.parseJSON(adminUser.details);
user.details.mon_groups = adminUserDetails.mon_groups;
}else{
failedAuthentication(req.body.function)
return failedAuthentication(req.body.function,req.body.mail)
}
}
if(user.details.factorAuth === "1"){
const factorAuthCreationResponse = createTwoFactorAuth(
user,
req.body.machineID || s.md5(s.gid()),
req.body.function
);
if(!factorAuthCreationResponse.goToDashboard){
renderPage(config.renderPaths.factorAuth,{
$user:{
ke: user.ke,
uid: user.uid,
mail: user.mail
},
lang: user.lang
})
return;
}
}
checkRoute(req.body.function,{
ok: true,
auth_token: user.auth,
ke: user.ke,
uid: user.uid,
mail: user.mail,
details: user.details
})
}else{
factorAuth()
failedAuthentication(req.body.function,req.body.mail)
}
}else{
failedAuthentication(req.body.function)
}
})
}
if(LdapAuth&&req.body.function==='ldap'&&req.body.key!==''){
s.knexQuery({
@ -500,7 +505,7 @@ module.exports = function(s,config,lang,app,io){
if(!user.uid){
user.uid=s.gid()
}
response = {
const userInfo = {
ke:req.body.key,
uid:user.uid,
auth:s.createHash(s.gid()),
@ -526,143 +531,89 @@ module.exports = function(s,config,lang,app,io){
if(rr&&rr[0]){
//already registered
rr = rr[0]
response = rr;
userInfo = rr;
rr.details = JSON.parse(rr.details)
response.lang = s.getLanguageFile(rr.details.lang)
userInfo.lang = s.getLanguageFile(rr.details.lang)
s.userLog({ke:req.body.key,mid:'$USER'},{type:r.lang['LDAP User Authenticated'],msg:{user:user,shinobiUID:rr.uid}})
s.knexQuery({
action: "update",
table: "Users",
update: {
auth: response.auth
auth: userInfo.auth
},
where: [
['ke','=',response.ke],
['ke','=',userInfo.ke],
['uid','=',rr.uid],
]
})
}else{
//new ldap login
s.userLog({ke:req.body.key,mid:'$USER'},{type:r.lang['LDAP User is New'],msg:{info:r.lang['Creating New Account'],user:user}})
response.lang = r.lang
userInfo.lang = r.lang
s.knexQuery({
action: "insert",
table: "Users",
insert: response,
insert: userInfo,
})
}
response.details = JSON.stringify(response.details)
response.auth_token = response.auth
response.ok = true
checkRoute(response)
userInfo.details = JSON.stringify(userInfo.details)
userInfo.auth_token = userInfo.auth
userInfo.ok = true
checkRoute(req.body.function,userInfo)
})
return
}
s.userLog({ke:req.body.key,mid:'$USER'},{type:r.lang['LDAP Failed'],msg:{err:err}})
//no user
req.default()
regularLogin()
});
req.auth.close(function(err) {
})
}else{
req.default()
regularLogin()
}
}else{
req.default()
regularLogin()
}
})
}else if(req.body.function === 'super'){
const superLoginResponse = await superLogin(req.body.mail,req.body.pass);
if(superLoginResponse.ok){
renderPage(config.renderPaths.super,{
config: config,
lang: lang,
$user: superLoginResponse.user,
customAutoLoad: s.customAutoLoadTree,
currentVersion: s.currentVersion,
})
}else{
if(req.body.function === 'super'){
if(!fs.existsSync(s.location.super)){
res.end(lang.superAdminText)
return
}
var ok = s.superAuth({
mail: req.body.mail,
pass: req.body.pass,
users: true,
md5: true
},function(data){
s.knexQuery({
action: "select",
columns: "*",
table: "Logs",
where: [
['ke','=','$'],
],
orderBy: ['time','desc'],
limit: 30
},(err,r) => {
if(!r){
r=[]
}
data.Logs = r
data.customAutoLoad = s.customAutoLoadTree
data.currentVersion = s.currentVersion
fs.readFile(s.location.config,'utf8',function(err,file){
data.plainConfig = JSON.parse(file)
renderPage(config.renderPaths.super,data)
})
})
})
if(ok === false){
failedAuthentication(req.body.function)
failedAuthentication(req.body.function,req.body.mail)
}
}else{
req.default()
regularLogin()
}
}
}else{
if(req.body.machineID&&req.body.factorAuthKey){
if(s.factorAuth[req.body.ke]&&s.factorAuth[req.body.ke][req.body.id]&&s.factorAuth[req.body.ke][req.body.id].key===req.body.factorAuthKey){
if(s.factorAuth[req.body.ke][req.body.id].key===req.body.factorAuthKey){
if(req.body.remember==="1"){
req.details=JSON.parse(s.factorAuth[req.body.ke][req.body.id].info.details)
req.lang=s.getLanguageFile(req.details.lang)
if(!req.details.acceptedMachines||!(req.details.acceptedMachines instanceof Object)){
req.details.acceptedMachines={}
}
if(!req.details.acceptedMachines[req.body.machineID]){
req.details.acceptedMachines[req.body.machineID]={}
s.knexQuery({
action: "update",
table: "Users",
update: {
details: s.prettyPrint(req.details)
},
where: [
['ke','=',req.body.ke],
['uid','=',req.body.id],
]
})
}
}
req.body.function = s.factorAuth[req.body.ke][req.body.id].function
response = s.factorAuth[req.body.ke][req.body.id].info
response.lang = req.lang || s.getLanguageFile(JSON.parse(s.factorAuth[req.body.ke][req.body.id].info.details).lang)
checkRoute(s.factorAuth[req.body.ke][req.body.id].info)
clearTimeout(s.factorAuth[req.body.ke][req.body.id].expireAuth)
s.deleteFactorAuth({
}else if(
req.body.machineID &&
req.body.factorAuthKey &&
s.factorAuth[req.body.ke] &&
s.factorAuth[req.body.ke][req.body.id]
){
const factorAuthObject = s.factorAuth[req.body.ke][req.body.id]
const twoFactorVerificationResponse = twoFactorVerification({
ke: req.body.ke,
uid: req.body.id,
id: req.body.id,
machineID: req.body.machineID,
factorAuthKey: req.body.factorAuthKey,
})
if(twoFactorVerificationResponse.ok){
checkRoute(twoFactorVerificationResponse.pageTarget,twoFactorVerificationResponse.info)
}else{
var info = s.factorAuth[req.body.ke][req.body.id].info
renderPage(config.renderPaths.factorAuth,{$user:{
ke: info.ke,
id: info.uid,
mail: info.mail,
},lang:req.lang});
res.end();
failedAuthentication(lang['2-Factor Authentication'],factorAuthObject.info.mail)
}
}else{
failedAuthentication(lang['2-Factor Authentication'])
}
}else{
failedAuthentication(lang['2-Factor Authentication'])
}
failedAuthentication(lang['2-Factor Authentication'],req.body.mail)
}
})
/**
@ -1794,6 +1745,76 @@ module.exports = function(s,config,lang,app,io){
},res,req);
})
/**
* API : Get Login Tokens
*/
app.get(config.webPaths.apiPrefix+':auth/loginTokens/:ke', function (req,res){
var response = {ok:false};
res.setHeader('Content-Type', 'application/json');
s.auth(req.params,(user) => {
const groupKey = req.params.ke
s.knexQuery({
action: "select",
columns: "*",
table: "LoginTokens",
where: [
['ke','=',groupKey],
['uid','=',user.uid],
]
},(err,rows) => {
response.ok = true
response.rows = rows
s.closeJsonResponse(res,response)
})
},res,req);
});
/**
* API : Get Login Token
*/
app.get(config.webPaths.apiPrefix+':auth/loginTokens/:ke/:loginId', function (req,res){
var response = {ok:false};
res.setHeader('Content-Type', 'application/json');
s.auth(req.params,(user) => {
const groupKey = req.params.ke
s.knexQuery({
action: "select",
columns: "*",
table: "LoginTokens",
where: [
['loginId','=',user.uid],
['ke','=',groupKey],
['uid','=',user.uid],
],
limit: 1
},(err,rows) => {
response.ok = !!rows[0]
response.row = rows[0]
s.closeJsonResponse(res,response)
})
},res,req);
});
/**
* API : Delete Login Token
*/
app.get(config.webPaths.apiPrefix+':auth/loginTokens/:ke/:loginId/delete', function (req,res){
var response = {ok:false};
res.setHeader('Content-Type', 'application/json');
s.auth(req.params,async (user) => {
const loginId = req.params.loginId
const groupKey = req.params.ke
const deleteResponse = await s.knexQueryPromise({
action: "delete",
table: "LoginTokens",
where: [
['loginId','=',loginId],
['ke','=',groupKey],
['uid','=',user.uid],
]
})
response.ok = true
s.closeJsonResponse(res,response)
},res,req);
});
/**
* API : Stream In to push data to ffmpeg by HTTP
*/
app.all('/:auth/streamIn/:ke/:id', function (req, res) {

View File

@ -2,7 +2,6 @@ var fs = require('fs');
var os = require('os');
var moment = require('moment')
var request = require('request')
var jsonfile = require("jsonfile")
var exec = require('child_process').exec;
var spawn = require('child_process').spawn;
var execSync = require('child_process').execSync;
@ -17,24 +16,23 @@ module.exports = function(s,config,lang,app){
app.all([config.webPaths.superApiPrefix+':auth/logs'], function (req,res){
req.ret={ok:false};
s.superAuth(req.params,function(resp){
const monitorRestrictions = s.getMonitorRestrictions(user.details,req.params.id)
s.getDatabaseRows({
monitorRestrictions: monitorRestrictions,
table: 'Logs',
groupKey: req.params.ke,
groupKey: '$',
date: req.query.date,
startDate: req.query.start,
endDate: req.query.end,
startOperator: req.query.startOperator,
endOperator: req.query.endOperator,
limit: req.query.limit,
archived: req.query.archived,
endIsStartTo: true
limit: req.query.limit || 30,
},(response) => {
response.rows.forEach(function(v,n){
r[n].info = JSON.parse(v.info)
response.rows[n].info = JSON.parse(v.info)
})
s.closeJsonResponse(res,{
ok: response.ok,
logs: response.rows
})
s.closeJsonResponse(res,r)
})
},res,req)
})
@ -102,9 +100,21 @@ module.exports = function(s,config,lang,app){
},res,req)
})
/**
* API : Superuser : Get Configuration (conf.json)
*/
app.get(config.webPaths.superApiPrefix+':auth/system/configure', function (req,res){
s.superAuth(req.params,async (resp) => {
var endData = {
ok: true,
config: JSON.parse(fs.readFileSync(s.location.config)),
}
s.closeJsonResponse(res,endData)
},res,req)
})
/**
* API : Superuser : Modify Configuration (conf.json)
*/
app.all(config.webPaths.superApiPrefix+':auth/system/configure', function (req,res){
app.post(config.webPaths.superApiPrefix+':auth/system/configure', function (req,res){
s.superAuth(req.params,async (resp) => {
var endData = {
ok : true
@ -117,7 +127,7 @@ module.exports = function(s,config,lang,app){
s.systemLog('conf.json Modified',{
by: resp.$user.mail,
ip: resp.ip,
old:jsonfile.readFileSync(s.location.config)
old: s.parseJSON(fs.readFileSync(s.location.config),{})
})
const configError = await modifyConfiguration(postBody)
if(configError)s.systemLog(configError)

View File

@ -1,8 +1,8 @@
-- --------------------------------------------------------
-- Host: 192.168.1.31
-- Server version: 10.1.30-MariaDB-0ubuntu0.17.10.1 - Ubuntu 17.10
-- Host: 172.16.100.238
-- Server version: 10.3.25-MariaDB-0ubuntu0.20.04.1 - Ubuntu 20.04
-- Server OS: debian-linux-gnu
-- HeidiSQL Version: 9.4.0.5125
-- HeidiSQL Version: 11.0.0.5919
-- --------------------------------------------------------
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
@ -20,13 +20,27 @@ USE `ccio`;
CREATE TABLE IF NOT EXISTS `API` (
`ke` varchar(50) DEFAULT NULL,
`uid` varchar(50) DEFAULT NULL,
`ip` tinytext,
`ip` tinytext DEFAULT NULL,
`code` varchar(100) DEFAULT NULL,
`details` text,
`time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
`details` text DEFAULT NULL,
`time` timestamp NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Data exporting was unselected.
-- Dumping structure for table ccio.Cloud Timelapse Frames
CREATE TABLE IF NOT EXISTS `Cloud Timelapse Frames` (
`ke` varchar(50) NOT NULL,
`mid` varchar(50) NOT NULL,
`href` text NOT NULL,
`details` longtext DEFAULT NULL,
`filename` varchar(50) NOT NULL,
`time` timestamp NULL DEFAULT NULL,
`size` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Data exporting was unselected.
-- Dumping structure for table ccio.Cloud Videos
CREATE TABLE IF NOT EXISTS `Cloud Videos` (
`mid` varchar(50) NOT NULL,
@ -35,59 +49,144 @@ CREATE TABLE IF NOT EXISTS `Cloud Videos` (
`size` float DEFAULT NULL,
`time` timestamp NULL DEFAULT NULL,
`end` timestamp NULL DEFAULT NULL,
`status` int(1) DEFAULT '0' COMMENT '0:Complete,1:Read,2:Archive',
`details` text
`status` int(1) DEFAULT 0 COMMENT '0:Complete,1:Read,2:Archive',
`details` text DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Data exporting was unselected.
-- Dumping structure for table ccio.Events
CREATE TABLE IF NOT EXISTS `Events` (
`ke` varchar(50) DEFAULT NULL,
`mid` varchar(50) DEFAULT NULL,
`details` text,
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
`details` text DEFAULT NULL,
`time` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
KEY `events_index` (`ke`,`mid`,`time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
-- Data exporting was unselected.
-- Dumping structure for table ccio.Events Counts
CREATE TABLE IF NOT EXISTS `Events Counts` (
`ke` varchar(50) NOT NULL,
`mid` varchar(50) NOT NULL,
`details` longtext NOT NULL,
`time` timestamp NOT NULL DEFAULT current_timestamp(),
`end` timestamp NOT NULL DEFAULT current_timestamp(),
`count` int(10) NOT NULL DEFAULT 1,
`tag` varchar(30) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Data exporting was unselected.
-- Dumping structure for table ccio.Files
CREATE TABLE IF NOT EXISTS `Files` (
`ke` varchar(50) NOT NULL,
`mid` varchar(50) NOT NULL,
`name` tinytext NOT NULL,
`size` float NOT NULL DEFAULT 0,
`details` text NOT NULL,
`status` int(1) NOT NULL DEFAULT 0,
`time` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Data exporting was unselected.
-- Dumping structure for table ccio.LoginTokens
CREATE TABLE IF NOT EXISTS `LoginTokens` (
`loginId` varchar(255) DEFAULT '',
`type` varchar(25) DEFAULT '',
`ke` varchar(50) DEFAULT '',
`uid` varchar(50) DEFAULT '',
`name` varchar(50) DEFAULT 'Unknown',
`lastLogin` timestamp NOT NULL DEFAULT current_timestamp(),
UNIQUE KEY `logintokens_loginid_unique` (`loginId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Data exporting was unselected.
-- Dumping structure for table ccio.Logs
CREATE TABLE IF NOT EXISTS `Logs` (
`ke` varchar(50) DEFAULT NULL,
`mid` varchar(50) DEFAULT NULL,
`info` text,
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
`info` text DEFAULT NULL,
`time` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
KEY `logs_index` (`ke`,`mid`,`time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Data exporting was unselected.
-- Dumping structure for table ccio.Monitors
CREATE TABLE IF NOT EXISTS `Monitors` (
`mid` varchar(50) DEFAULT NULL,
`ke` varchar(50) DEFAULT NULL,
`name` varchar(50) DEFAULT NULL,
`shto` text,
`shfr` text,
`details` longtext,
`shto` text DEFAULT NULL,
`shfr` text DEFAULT NULL,
`details` longtext DEFAULT NULL,
`type` varchar(50) DEFAULT 'jpeg',
`ext` varchar(50) DEFAULT 'webm',
`protocol` varchar(50) DEFAULT 'http',
`host` varchar(100) DEFAULT '0.0.0.0',
`path` varchar(100) DEFAULT '/',
`port` int(8) DEFAULT '80',
`fps` int(8) DEFAULT '1',
`port` int(8) DEFAULT 80,
`fps` int(8) DEFAULT 1,
`mode` varchar(15) DEFAULT NULL,
`width` int(11) DEFAULT '640',
`height` int(11) DEFAULT '360'
`width` int(11) DEFAULT 640,
`height` int(11) DEFAULT 360,
KEY `monitors_index` (`ke`,`mode`,`type`,`ext`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Data exporting was unselected.
-- Dumping structure for table ccio.Presets
CREATE TABLE IF NOT EXISTS `Presets` (
`ke` varchar(50) DEFAULT NULL,
`name` text,
`details` text,
`type` enum('monitor','event','user') DEFAULT NULL
`name` text DEFAULT NULL,
`details` text DEFAULT NULL,
`type` varchar(50) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Data exporting was unselected.
-- Dumping structure for table ccio.Schedules
CREATE TABLE IF NOT EXISTS `Schedules` (
`ke` varchar(50) DEFAULT NULL,
`name` text DEFAULT NULL,
`details` text DEFAULT NULL,
`start` varchar(10) DEFAULT NULL,
`end` varchar(10) DEFAULT NULL,
`enabled` int(1) NOT NULL DEFAULT 1
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Data exporting was unselected.
-- Dumping structure for table ccio.Timelapse Frames
CREATE TABLE IF NOT EXISTS `Timelapse Frames` (
`ke` varchar(50) NOT NULL,
`mid` varchar(50) NOT NULL,
`details` longtext DEFAULT NULL,
`filename` varchar(50) NOT NULL,
`time` timestamp NULL DEFAULT NULL,
`size` int(11) NOT NULL,
KEY `timelapseframes_index` (`ke`,`mid`,`time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Data exporting was unselected.
-- Dumping structure for table ccio.Timelapses
CREATE TABLE IF NOT EXISTS `Timelapses` (
`ke` varchar(50) NOT NULL,
`mid` varchar(50) NOT NULL,
`details` longtext DEFAULT NULL,
`date` date NOT NULL,
`time` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
`end` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
`size` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Data exporting was unselected.
-- Dumping structure for table ccio.Users
CREATE TABLE IF NOT EXISTS `Users` (
`ke` varchar(50) DEFAULT NULL,
@ -95,12 +194,13 @@ CREATE TABLE IF NOT EXISTS `Users` (
`auth` varchar(50) DEFAULT NULL,
`mail` varchar(100) DEFAULT NULL,
`pass` varchar(100) DEFAULT NULL,
`accountType` int(1) DEFAULT '0',
`details` longtext,
`accountType` int(1) DEFAULT 0,
`details` longtext DEFAULT NULL,
UNIQUE KEY `mail` (`mail`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Data exporting was unselected.
-- Dumping structure for table ccio.Videos
CREATE TABLE IF NOT EXISTS `Videos` (
`mid` varchar(50) DEFAULT NULL,
@ -111,69 +211,14 @@ CREATE TABLE IF NOT EXISTS `Videos` (
`size` float DEFAULT NULL,
`frames` int(11) DEFAULT NULL,
`end` timestamp NULL DEFAULT NULL,
`status` int(1) DEFAULT '0',
`archived` int(1) DEFAULT '0',
`details` text
`status` int(1) DEFAULT 0,
`archived` int(1) DEFAULT 0,
`details` text DEFAULT NULL,
KEY `videos_index` (`time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Data exporting was unselected.
-- Dumping structure for table ccio.Files
CREATE TABLE IF NOT EXISTS `Files` (
`ke` varchar(50) NOT NULL,
`mid` varchar(50) NOT NULL,
`name` tinytext NOT NULL,
`size` float NOT NULL DEFAULT '0',
`details` text NOT NULL,
`status` int(1) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `Files` ADD COLUMN `time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `status`;
-- Data exporting was unselected.
-- Dumping structure for table ccio.Schedules
CREATE TABLE IF NOT EXISTS `Schedules` (
`ke` varchar(50) DEFAULT NULL,
`name` text,
`details` text,
`start` varchar(10) DEFAULT NULL,
`end` varchar(10) DEFAULT NULL,
`enabled` int(1) NOT NULL DEFAULT '1'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Dumping structure for table ccio.Timelapses
CREATE TABLE IF NOT EXISTS `Timelapses` (
`ke` varchar(50) NOT NULL,
`mid` varchar(50) NOT NULL,
`details` longtext,
`date` date NOT NULL,
`time` timestamp NOT NULL,
`end` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`size` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Dumping structure for table ccio.Timelapse Frames
CREATE TABLE IF NOT EXISTS `Timelapse Frames` (
`ke` varchar(50) NOT NULL,
`mid` varchar(50) NOT NULL,
`details` longtext,
`filename` varchar(50) NOT NULL,
`time` timestamp NULL DEFAULT NULL,
`size` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Dumping structure for table ccio.Timelapse Frames
CREATE TABLE IF NOT EXISTS `Cloud Timelapse Frames` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`href` text NOT NULL,`details` longtext,`filename` varchar(50) NOT NULL,`time` timestamp NULL DEFAULT NULL,`size` int(11) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Dumping structure for table ccio.Events Counts
CREATE TABLE IF NOT EXISTS `Events Counts` (
`ke` varchar(50) NOT NULL,
`mid` varchar(50) NOT NULL,
`details` longtext NOT NULL,
`time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`end` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`count` int(10) NOT NULL DEFAULT 1,
`tag` varchar(30) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Data exporting was unselected.
/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS=IF(@OLD_FOREIGN_KEY_CHECKS IS NULL, 1, @OLD_FOREIGN_KEY_CHECKS) */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;

View File

@ -0,0 +1,57 @@
$(document).ready(function(){
var alternateLoginsBox = $('#alternate-logins')
function getAlternateLogins(){
$.get(getApiPrefix('loginTokens'),function(data){
var rows = data.rows
alternateLoginsBox.empty()
if(rows.length > 0){
$.each(rows,function(n,row){
alternateLoginsBox.append(`<div class="row" login-id="${row.loginId}">
<div class="col-md-4" style="text-transform:capitalize;font-size: 150%;">
<i class="fa fa-${row.type}"></i> &nbsp;
<span>${row.type}</span>
</div>
<div class="col-md-4">
<div>${row.name}</div>
<div title="${lang.lastLogin}">${moment(row.lastLogin).format('YYYY-MM-DD hh:mm:ss A')}</div>
</div>
<div class="col-md-4 text-right">
<a class="btn btn-sm btn-danger unlink-account"><i class="fa fa-unlink"></i> ${lang.Unlink}</a>
</div>
</div>`)
})
}else{
alternateLoginsBox.append(`<div class="row">
<div class="col-md-12 text-center epic-text" style="margin: 0">
${lang.noLoginTokensAdded}
</div>
</div>`)
}
})
}
getAlternateLogins()
alternateLoginsBox.on('click','.unlink-account',function(){
var loginId = $(this).parents('[login-id]').attr('login-id')
$.confirm.create({
title: lang['Unlink Login'],
body: lang.noUndoForAction,
clickOptions: {
title: lang['Unlink'],
class: 'btn-danger'
},
clickCallback: function(){
$.get(getApiPrefix('loginTokens') + '/' + loginId + '/delete',function(data){
if(data.ok){
new PNotify({
title: lang.Unlinked,
text: lang.loginHandleUnbound,
type: 'success'
})
alternateLoginsBox.find(`[login-id="${loginId}"]`).remove()
}
})
}
})
})
window.drawAlternateLoginsToSettings = getAlternateLogins
})

View File

@ -31,6 +31,8 @@ $(document).ready(function(){
$.confirm.body.css('word-wrap','break-word')
}
$.confirm.body.html(options.body)
}else{
alert('No Title, Language file Update?')
}
if(options.clickOptions && options.clickCallback)$.confirm.click(options.clickOptions,options.clickCallback)
}

View File

@ -0,0 +1,13 @@
$(document).ready(function(){
$('#settings').on('click','.google-sign-in',function(){
var signInWindow = window.open(getApiPrefix('loginTokenAddGoogle'),'popup','width=300,height=300,scrollbars=no,resizable=no');
if(!signInWindow || signInWindow.closed || typeof signInWindow.closed=='undefined'){
alert(`Your Popup Blocker is disabling this feature.`)
}else{
signInWindow.onbeforeunload = function(){
drawAlternateLoginsToSettings()
}
}
return false;
})
})

View File

@ -267,8 +267,11 @@ $(document).ready(function(){
schema: schema
});
configurationEditor.setValue(shinobiConfig);
function loadConfiguationIntoEditor(){
$.get(superApiPrefix + $user.sessionKey + '/system/configure',function(data){
configurationEditor.setValue(data.config);
})
}
// configurationEditor.on("change", function() {
// // Do something...
// });
@ -307,5 +310,12 @@ $(document).ready(function(){
submitConfiguration()
return false;
})
$.ccio.ws.on('f',function(d){
switch(d.f){
case'init_success':
loadConfiguationIntoEditor()
break;
}
})
window.configurationEditor = configurationEditor
})

View File

@ -224,7 +224,7 @@ $.aN.f.submit(function(e){
//client side email check
$.aN.e.on('change','[name="mail"]',function(){
var thisVal = $(this).val()
$.each(users,function(n,user){
$.each(loadedUsers,function(n,user){
if($.aN.selected && user.ke !== $.aN.selected.ke && thisVal.toLowerCase() === user.mail.toLowerCase()){
new PNotify({text:lang['Email address is in use.'],type:'error'})
}
@ -233,7 +233,7 @@ $.aN.e.on('change','[name="mail"]',function(){
//client side group key check
$.aN.e.on('change','[name="ke"]',function(){
var thisVal = $(this).val()
$.each(users,function(n,user){
$.each(loadedUsers,function(n,user){
if(!$.aN.modeIsEdit() && user.ke === thisVal){
new PNotify({text:lang['Group Key is in use.'] + ' ' + lang['Create Sub-Accounts at /admin'],type:'error'})
}

View File

@ -269,3 +269,4 @@
</div>
</div>
<script src="<%-window.libURL%>libs/js/dash2.usersettings.js"></script>
<script src="<%-window.libURL%>libs/js/dash2.alternateLogins.js"></script>

View File

@ -202,6 +202,7 @@ var mediaRecorder;
var chunks = [];
var count = 0;
function initVideoStream(){
$.ccio.vid = {element:$('#video')[0],canvas:$('#canvas')[0],data:$('#data')};
$.ccio.vid.element.controls = false;
navigator.getUserMedia(constraints,function(stream,fn) {
@ -209,7 +210,7 @@ navigator.getUserMedia(constraints,function(stream,fn) {
$.ccio.vid.element.srcObject = stream;
$('[record]').click($.ccio.startSending)
////////
if($user.mons.length>0&&$.ls.getItem('Shinobi_Dashcam')){
if(loadedMonitors.length>0&&$.ls.getItem('Shinobi_Dashcam')){
$('[monitor="'+$.ls.getItem('Shinobi_Dashcam')+'"]').click()
if($.ls.getItem('Shinobi_Dashcam_Started') === 1){
setTimeout(function(){
@ -220,32 +221,40 @@ navigator.getUserMedia(constraints,function(stream,fn) {
$.ccio.selected = null;
}
}, function(err){console.error('getUserMedia',err)});
}
//draw selectable mons
var loadedMonitors = []
function drawMonitorList(callback){
var tmp='';
if($user.mons&&$user.mons.length>0){
$('#monitors').empty()
$.get($user.auth_token + '/monitor/' + $user.ke,function(monitors){
loadedMonitors = [];
if(monitors && monitors.length > 0){
$.ccio.mon={};
$.each($user.mons,function(n,v){
$.each(monitors,function(n,v){
if(v.type === 'dashcam'){
loadedMonitors.push(v)
v.details = JSON.parse(v.details);
$.ccio.mon[v.mid] = v;
tmp += '<a class="list-group-item" monitor="'+v.mid+'">'+v.name+'</a>';
}
})
$('#monitors').html(tmp)
}else{
tmp+="<h2>No Streamer Monitors Setup</h2>"
tmp+="<h2>No Dashcam Monitors Setup</h2>"
tmp+="<small>Login to the Dashboard and add one. Set it to record or watch only.</small>"
$('#msg').html(tmp)
}
delete(tmp);
callback(monitors)
})
}
$(document).ready(function(){
drawMonitorList(function(){
initVideoStream()
})
})
$('body').on('click','[monitor]',function(e){
e.e=$(this);e.a=e.e.attr('monitor'),e.m=$.ccio.mon[e.a];
$.ccio.selected=e.a;$('#selected').html(e.a);

View File

@ -1,5 +1,5 @@
<%
var details = JSON.parse($user.details)
var details = $user.details
if(!details.sub){ %>
<script>
window.getAdminApiPrefix = function(piece){

View File

@ -1,5 +1,6 @@
<% include blocks/header %>
<link rel="stylesheet" href="<%-window.libURL%>libs/themes/Ice/style.css">
<meta name="google-signin-client_id" content="<%- config.appIdGoogleSignIn %>">
<style>
.wide-text{
text-transform: uppercase;
@ -67,7 +68,7 @@
<form id="login-form" method="post" style="display: block;margin:0">
<input type="hidden" name="machineID" id="machineID" value="">
<% var message,timeLeft;if(message){ %>
<div class="form-group text-center monospace">
<div class="form-group text-center monospace" id ="login-message">
<%= message %>
</div>
<% } %>
@ -80,6 +81,13 @@
<div class="form-group f_i_input f_i_ldap" style="display:none">
<input name="key" id="key" tabindex="2" class="monospace form-control wide-text" placeholder="Group Key">
</div>
<div class="form-group" style="display:none">
<select class="form-control wide-text" name="alternateLogin">
<option value="" selected>Default</option>
<option value="google">Google</option>
</select>
<input style="display:none" name="alternateLoginToken" class="monospace form-control wide-text" placeholder="Group Key">
</div>
<% if(config.showLoginSelector === true){ %>
<div class="form-group">
<div class="row">
@ -123,6 +131,11 @@
<div class="form-group">
<button type="submit" name="login-submit" id="login-submit" tabindex="4" class="btn btn-success btn-block wide-text" style="color:#FFF"><i class="fa fa-key"></i> <%- lang.Login %></button>
</div>
<% if(config.allowGoogleSignOn){ %>
<div class="form-group text-center">
<div class="g-signin2" data-onsuccess="onGoogleSignIn" style="display: inline-block;"></div>
</div>
<% } %>
<div class="form-group text-center" style="margin:0">
<span style="<%- config.poweredByShinobiClass %>;margin-right: 10px" class="epic-text text-green"><i class="fa fa-sign-in"></i> <%- lang['Remember Me'] %></span>
<div class="text-right" title="<%- lang['Remember Me'] %>" style="display:inline-block">
@ -149,6 +162,7 @@
</div>
<script src="<%-window.libURL%>libs/js/material.min.js"></script>
<script>
var googleSignIn = false;
<% var failedLogin;if(failedLogin===true){ %>
localStorage.removeItem('ShinobiLogin_'+location.host)
<% } %>
@ -167,11 +181,13 @@
$('#machineID').val($.ccio.auth)
})
$.ccio.f.submit(function(e){
$('#login-message').remove()
$('input').css('border-color','')
e.e=$(this),e.s=e.e.serializeObject(),e.inputs=e.e.find('input,button');
if(e.s.remember){
localStorage.setItem('ShinobiLogin_'+location.host,JSON.stringify({mail:e.s.mail,pass:e.s.pass,function:e.s.function}))
}else{localStorage.removeItem('ShinobiLogin_'+location.host)}
if(googleSignIn)googleSignOut()
})
if($.ccio.ls){
$.ccio.ls=JSON.parse($.ccio.ls);
@ -212,3 +228,23 @@ $('[selector]').change(function(e){
$('.'+e.a+'_text').text($(this).find('option:selected').text())
}).change();
</script>
<% if(config.allowGoogleSignOn){ %>
<script src="https://apis.google.com/js/platform.js" async defer></script>
<script>
function onGoogleSignIn(googleUser) {
var id_token = googleUser.getAuthResponse().id_token;
$.ccio.f.find('[name="mail"],[name="pass"],.g-signin2').hide()
$.ccio.f.find('[name="alternateLogin"]').val('google')
$.ccio.f.find('[name="alternateLoginToken"]').val(id_token)
$.ccio.f.find('[name="login-submit"]').html(`<i class="fa fa-google"></i> <%- lang.Login %>`)
googleSignIn = true
$.ccio.f.submit()
}
function googleSignOut() {
var auth2 = gapi.auth2.getAuthInstance();
auth2.signOut().then(function () {
console.log('Google Signed out.');
});
}
</script>
<% } %>

View File

@ -0,0 +1,45 @@
<%
window.libURL = originalURL + global.s.checkCorrectPathEnding(config.webPaths.home)
%>
<meta name="google-signin-client_id" content="<%- config.appIdGoogleSignIn %>">
<script src="<%-window.libURL%>libs/js/jquery.min.js"></script>
<style>
body {
background: #333;
}
.g-signin2 {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
<div class="g-signin2" data-onsuccess="onGoogleSignIn" style="display: inline-block;"></div>
<script src="https://apis.google.com/js/platform.js" async defer></script>
<script>
function onGoogleSignIn(googleUser) {
console.log('Logged in to Google! Binding...')
var id_token = googleUser.getAuthResponse().id_token;
$.post(location.href,{
loginToken: id_token,
},function(data){
googleSignOut()
if(data.ok){
window.close()
}else{
console.log(data)
$('.g-signin2').html(data.msg || 'Failed to Save').css({
color: "#fff",
textAlign: "center",
fontFamily: "monospace",
})
}
})
}
function googleSignOut() {
var auth2 = gapi.auth2.getAuthInstance();
auth2.signOut().then(function () {
console.log('Google Signed out.');
});
}
</script>

View File

@ -74,12 +74,11 @@ $.ccio.start=function(){
function initVideoStream(){
$.ccio.vid = {e:$('#video'),c:$('#canvas')};
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia||navigator.mediaDevices.getUserMedia;
window.URL = window.URL || window.webkitURL || window.mozURL || window.msURL;
navigator.getUserMedia({video: true},function(stream,fn) {
//set video element
if ($.ccio.vid.e[0].mozSrcObject !== undefined) {
$.ccio.vid.e[0].mozSrcObject = stream;
} else {
@ -92,24 +91,23 @@ $.ccio.vid = {e:$('#video'),c:$('#canvas')};
$('[record]').click($.ccio.start)
})
}, function(err){console.error('getUserMedia',err)});
}
//draw selectable mons
var loadedMonitors = []
function drawMonitorList(callback){
var tmp='';
if($user.mons&&$user.mons.length>0){
$('#monitors').empty()
$.get($user.auth_token + '/monitor/' + $user.ke,function(monitors){
loadedMonitors = [];
if(monitors && monitors.length > 0){
$.ccio.mon={};
$.each($user.mons,function(n,v){
$.each(monitors,function(n,v){
if(v.type === 'streamer'){
loadedMonitors.push(v)
v.details = JSON.parse(v.details);
$.ccio.mon[v.mid] = v;
tmp += '<a class="list-group-item" monitor="'+v.mid+'">'+v.name+'</a>';
}
})
$('#monitors').html(tmp)
}else{
@ -117,7 +115,20 @@ $.ccio.vid = {e:$('#video'),c:$('#canvas')};
tmp+="<small>Login to the Dashboard and add one. Set it to record or watch only.</small>"
$('#msg').html(tmp)
}
delete(tmp);
callback(monitors)
})
}
$(document).ready(function(){
drawMonitorList(function(){
initVideoStream()
if(loadedMonitors.length > 0 && $.ls.getItem('Shinobi_socket_camera')){
$('[monitor="'+$.ls.getItem('Shinobi_socket_camera')+'"]').click()
}else{
$.ccio.selected=null;
}
})
})
$('body').on('click','[monitor]',function(e){
e.e=$(this);e.a=e.e.attr('monitor'),e.m=$.ccio.mon[e.a];
$.ccio.selected=e.a;$('#selected').html(e.a);
@ -127,7 +138,6 @@ $.ccio.vid = {e:$('#video'),c:$('#canvas')};
})
if($user.mons.length>0&&$.ls.getItem('Shinobi_socket_camera')){$('[monitor="'+$.ls.getItem('Shinobi_socket_camera')+'"]').click()}else{$.ccio.selected=null;}
$('body')
.on('click','.logout',function(e){

View File

@ -34,7 +34,6 @@
</style>
<script>
var superApiPrefix = location.search === '?p2p=1' ? location.pathname + '/' : "<%=originalURL%><%=config.webPaths.superApiPrefix%>"
var shinobiConfig = <%- JSON.stringify(plainConfig) %>
</script>
<% customAutoLoad.superLibsCss.forEach(function(lib){ %>
<link rel="stylesheet" href="<%-window.libURL%>libs/css/<%-lib%>">
@ -171,6 +170,31 @@
</script>
<script>
var loadedUsers = {}
function drawUserList(){
loadedUsers = {}
$('#accounts table').empty()
$.get(superApiPrefix + $user.sessionKey + '/accounts/list/admin',function(data){
$.each(data.users,function(n,v){
loadedUsers[v.ke] = v
$.ccio.tm(0,v,'#accounts table')
})
})
}
function drawSystemLogs(){
$('#logs table').empty()
$.get(superApiPrefix + $user.sessionKey + '/logs',function(data){
if(data.ok){
$.each(data.logs.reverse(),function(n,v){
$.ccio.tm(4,v,'#logs table')
})
}else{
var html = `<div class="mt-3 mb-4"><p><i class="fa fa-4x fa-exclamation-circle text-danger"></i></p><h3>Database is not running or unreachable</h3><p>Please ensure it is started then restart Shinobi.</p><p>By default Shinobi uses <b>MariaDB</b>, an SQL server, as the database engine.</p><p>If you need to install the database files again you may follow <a target="_blank" href="https://shinobi.video/articles/2019-02-22-how-to-install-the-shinobi-database-manually">this article.</a></p></div>`
$('#main-card').html(html)
}
})
}
$.ccio={accounts:{}};$.ls=localStorage;
if(!$user.lang||$user.lang==''){
$user.lang="<%-config.language%>"
@ -202,6 +226,8 @@ $.ccio.ws.on('f',function(d){
switch(d.f){
case'init_success':
$user.sessionKey = d.superSessionKey
drawUserList()
drawSystemLogs()
break;
case'log':
$.ccio.tm(4,d.log,'#logs table')
@ -244,7 +270,7 @@ $.ccio.tm=function(x,d,z,k){
break;
case 4://log row, draw to global and monitor
if(!d.time){d.time=$.ccio.init('t')}
tmp+='<tr class="search-row"><td><span title="'+d.time+'" class="livestamp"></span><br><small>'+d.time+'</small><br><small>'+d.mid+'</small></td><td></td><td><pre class="pre-inline">'+$.ccio.init('jsontoblock',JSON.parse(d.info))+'</pre></td></tr>'
tmp+='<tr class="search-row"><td><span title="'+d.time+'" class="livestamp"></span><br><small>'+d.time+'</small><br><small>'+d.mid+'</small></td><td></td><td><pre class="pre-inline">'+$.ccio.init('jsontoblock',d.info)+'</pre></td></tr>'
break;
}
if(z){
@ -290,25 +316,6 @@ $.ccio.init=function(x,d,z,k){
//logs
$.logs={e:$('#logs')}
////
$(document).ready(function(){
<% if(!users || !(users instanceof Array)){ %>
var html = `<div class="mt-3 mb-4"><p><i class="fa fa-4x fa-exclamation-circle text-danger"></i></p><h3>Database is not running or unreachable</h3><p>Please ensure it is started then restart Shinobi.</p><p>By default Shinobi uses <b>MariaDB</b>, an SQL server, as the database engine.</p><p>If you need to install the database files again you may follow <a target="_blank" href="https://shinobi.video/articles/2019-02-22-how-to-install-the-shinobi-database-manually">this article.</a></p></div>`
$('#main-card').html(html)
<% }else{ %>
users = <%- JSON.stringify(users) %>;
if(users){
$.each(users,function(n,v){
$.ccio.tm(0,v,'#accounts table')
})
}else{
}
<% } %>
$.each(<%-JSON.stringify(Logs)%>,function(n,v){
$.ccio.tm(4,v,'#logs table')
})
})
//
$('body')
.on('click','.logout',function(e){
localStorage.removeItem('ShinobiLogin_'+location.host);location.href='/';