/* ============================================================ EATME PORTÁL – AUTH SERVICE ============================================================ */ /* ============================================================ AKTIVAČNÍ KÓD ============================================================ */ function requestActivation(email) { email = emailNorm_( email ); const user = findOne_( CFG.SHEETS.USERS, function(row) { return ( emailNorm_( row.email ) === email ); } ); if ( !user || !( user.active === true || String( user.active ).toUpperCase() === 'TRUE' ) ) { /* * Neprozrazujeme, zda účet existuje. */ return { ok:true }; } const code = randomCode_(); append_( CFG.SHEETS.LOGIN_CODES, { code_id: uuid_( 'CODE' ), email: email, purpose: 'ACTIVATE', code_hash: hashText_( code ), expires_at: new Date( Date.now() + CFG.LOGIN_CODE_MINUTES * 60000 ), used_at: '', created_at: now_() } ); MailApp.sendEmail( { to: email, subject: CFG.APP_NAME + ' – aktivace účtu', body: 'Váš aktivační kód je: ' + code + '\nPlatí ' + CFG.LOGIN_CODE_MINUTES + ' minut.', htmlBody: '

Váš aktivační kód:

' + '

' + code + '

' + '

Platí ' + CFG.LOGIN_CODE_MINUTES + ' minut.

', name: CFG.APP_NAME } ); return { ok:true }; } /* ============================================================ AKTIVACE ÚČTU ============================================================ */ function activateAccount( email, code, password ) { email = emailNorm_( email ); validatePassword_( password ); const user = findOne_( CFG.SHEETS.USERS, function(row) { return ( emailNorm_( row.email ) === email ); } ); if ( !user ) { throw new Error( 'Aktivaci nelze dokončit.' ); } const candidates = rows_( CFG.SHEETS.LOGIN_CODES ) .filter( function(row) { return ( emailNorm_( row.email ) === email && String( row.purpose ) === 'ACTIVATE' && !row.used_at ); } ) .sort( function(a,b) { return ( new Date( b.created_at ) - new Date( a.created_at ) ); } ); const record = candidates[0]; if ( !record || new Date( record.expires_at ).getTime() < Date.now() || String( record.code_hash ) !== hashText_( code ) ) { throw new Error( 'Kód je neplatný nebo vypršel.' ); } const salt = Utilities .getUuid() .replace( /-/g, '' ); updateBy_( CFG.SHEETS.USERS, 'user_id', user.user_id, { password_salt: salt, password_hash: passwordHash_( password, salt ) } ); updateBy_( CFG.SHEETS.LOGIN_CODES, 'code_id', record.code_id, { used_at: now_() } ); audit_( user.user_id, 'ACCOUNT_ACTIVATED', 'USER', user.user_id, '', '' ); return login( email, password ); } /* ============================================================ ZAPOMENUTÉ HESLO – POSLAT KÓD Odpověď je vždy stejná. Neprozrazujeme existenci účtu. ============================================================ */ function requestPasswordReset(email) { email = emailNorm_( email ); const genericResult = { ok:true }; if ( !email ) { return genericResult; } const user = findOne_( CFG.SHEETS.USERS, function(row) { return ( emailNorm_( row.email ) === email ); } ); if ( !user || !( user.active === true || String( user.active ).toUpperCase() === 'TRUE' ) ) { return genericResult; } const code = randomCode_(); append_( CFG.SHEETS.LOGIN_CODES, { code_id: uuid_( 'CODE' ), email: email, purpose: 'RESET_PASSWORD', code_hash: hashText_( code ), expires_at: new Date( Date.now() + CFG.LOGIN_CODE_MINUTES * 60000 ), used_at: '', created_at: now_() } ); MailApp.sendEmail( { to: email, subject: CFG.APP_NAME + ' – obnovení hesla', body: 'Obdrželi jsme žádost o změnu hesla k účtu ' + CFG.APP_NAME + '.\n\nOvěřovací kód: ' + code + '\n\nKód platí ' + CFG.LOGIN_CODE_MINUTES + ' minut.\n\nPokud jste o změnu hesla nežádali, tento e-mail ignorujte.', htmlBody: '

Obdrželi jsme žádost o změnu hesla k účtu ' + CFG.APP_NAME + '.

' + '

Ověřovací kód:

' + '

' + code + '

' + '

Kód platí ' + CFG.LOGIN_CODE_MINUTES + ' minut.

' + '

Pokud jste o změnu hesla nežádali, tento e-mail ignorujte.

', name: CFG.APP_NAME } ); audit_( user.user_id, 'PASSWORD_RESET_REQUESTED', 'USER', user.user_id, '', '' ); return genericResult; } /* ============================================================ ZAPOMENUTÉ HESLO – NASTAVIT NOVÉ HESLO ============================================================ */ function resetPassword( email, code, newPassword ) { email = emailNorm_( email ); validatePassword_( newPassword ); const user = findOne_( CFG.SHEETS.USERS, function(row) { return ( emailNorm_( row.email ) === email ); } ); /* * Úmyslně používáme stejnou chybu pro neexistující účet * i neplatný kód. */ if ( !user || !( user.active === true || String( user.active ).toUpperCase() === 'TRUE' ) ) { throw new Error( 'Kód je neplatný nebo vypršel.' ); } const candidates = rows_( CFG.SHEETS.LOGIN_CODES ) .filter( function(row) { return ( emailNorm_( row.email ) === email && String( row.purpose ) === 'RESET_PASSWORD' && !row.used_at ); } ) .sort( function(a,b) { return ( new Date( b.created_at ) - new Date( a.created_at ) ); } ); const record = candidates[0]; if ( !record || new Date( record.expires_at ).getTime() < Date.now() || String( record.code_hash ) !== hashText_( code ) ) { throw new Error( 'Kód je neplatný nebo vypršel.' ); } /* * Nový salt. Používáme stejný passwordHash_ jako dosud, * takže se nerozbije současná autentizace. */ const salt = Utilities .getUuid() .replace( /-/g, '' ); updateBy_( CFG.SHEETS.USERS, 'user_id', user.user_id, { password_salt: salt, password_hash: passwordHash_( newPassword, salt ) } ); /* * Spotřebujeme všechny dosud platné RESET_PASSWORD kódy * pro tento e-mail, ne pouze poslední. */ candidates.forEach( function(candidate) { updateBy_( CFG.SHEETS.LOGIN_CODES, 'code_id', candidate.code_id, { used_at: now_() } ); } ); /* * Zneplatníme VŠECHNY existující sessions uživatele. * Po resetu se musí všechna zařízení přihlásit znovu. */ rows_( CFG.SHEETS.SESSIONS ) .filter( function(session) { return ( String( session.user_id ) === String( user.user_id ) ); } ) .forEach( function(session) { updateBy_( CFG.SHEETS.SESSIONS, 'session_id', session.session_id, { expires_at: new Date( 0 ) } ); } ); audit_( user.user_id, 'PASSWORD_RESET_COMPLETED', 'USER', user.user_id, '', '' ); /* * Informační e-mail po změně hesla. */ try { MailApp.sendEmail( { to: email, subject: CFG.APP_NAME + ' – heslo bylo změněno', body: 'Heslo k vašemu účtu ' + CFG.APP_NAME + ' bylo právě změněno.\n\nVšechny předchozí relace byly odhlášeny.\n\nPokud jste tuto změnu neprovedli vy, kontaktujte administrátora.', htmlBody: '

Heslo k vašemu účtu ' + CFG.APP_NAME + ' bylo právě změněno.

' + '

Všechny předchozí relace byly odhlášeny.

' + '

Pokud jste tuto změnu neprovedli vy, kontaktujte administrátora.

', name: CFG.APP_NAME } ); } catch(error) { /* * Selhání informačního e-mailu nesmí vrátit zpět * už úspěšně změněné heslo. */ console.error( 'PASSWORD RESET CONFIRMATION EMAIL ERROR:', error ); } return { ok:true }; } /* ============================================================ LOGIN ============================================================ */ function login( email, password ) { email = emailNorm_( email ); const user = findOne_( CFG.SHEETS.USERS, function(row) { return ( emailNorm_( row.email ) === email ); } ); if ( !user || !user.password_salt || !user.password_hash ) { throw new Error( 'Neplatný e-mail nebo heslo.' ); } const calculatedHash = passwordHash_( password, user.password_salt ); if ( calculatedHash !== String( user.password_hash ) ) { throw new Error( 'Neplatný e-mail nebo heslo.' ); } let employee = null; if ( user.employee_id ) { employee = findOne_( CFG.SHEETS.EMPLOYEES, function(row) { return ( String( row.employee_id ) === String( user.employee_id ) ); } ); } const token = randomToken_(); const loginTime = now_(); append_( CFG.SHEETS.SESSIONS, { session_id: uuid_( 'SES' ), user_id: user.user_id, token_hash: hashText_( token ), expires_at: new Date( Date.now() + CFG.SESSION_DAYS * 86400000 ), created_at: loginTime, last_seen_at: loginTime } ); updateBy_( CFG.SHEETS.USERS, 'user_id', user.user_id, { last_login: loginTime } ); return { ok: true, token: token, role: user.role, user_id: user.user_id, user: { user_id: user.user_id, email: user.email, role: user.role, employee: employee ? { employee_id: employee.employee_id, first_name: employee.first_name, last_name: employee.last_name, company_id: employee.company_id, location_id: employee.location_id, position: employee.position } : null } }; } /* ============================================================ LOGOUT ============================================================ */ function logout(token) { const tokenHash = hashText_( String( token || '' ) ); const session = findOne_( CFG.SHEETS.SESSIONS, function(row) { return ( String( row.token_hash ) === tokenHash ); } ); if ( session ) { updateBy_( CFG.SHEETS.SESSIONS, 'session_id', session.session_id, { expires_at: new Date( 0 ) } ); } return { ok:true }; } /* ============================================================ AKTUÁLNÍ UŽIVATEL ============================================================ */ function me(token) { const user = requireUser_( token ); const employee = user.employee_id ? findOne_( CFG.SHEETS.EMPLOYEES, function(row) { return ( String( row.employee_id ) === String( user.employee_id ) ); } ) : null; return { user_id: user.user_id, email: user.email, role: user.role, employee: employee ? { employee_id: employee.employee_id, first_name: employee.first_name, last_name: employee.last_name, company_id: employee.company_id, location_id: employee.location_id, position: employee.position } : null }; }