1
0
Fork 0
mirror of synced 2024-06-18 18:54:55 +12:00
appwrite/app/controllers/api/account.php

519 lines
20 KiB
PHP
Raw Normal View History

2019-05-09 18:54:39 +12:00
<?php
2020-01-04 10:00:53 +13:00
global $utopia, $register, $request, $response, $user, $audit, $webhook, $project, $domain, $projectDB, $providers, $clients;
2019-05-09 18:54:39 +12:00
use Utopia\Exception;
use Utopia\Validator\Text;
use Utopia\Validator\Email;
2020-01-04 10:00:53 +13:00
use Utopia\Validator\WhiteList;
use Utopia\Validator\Host;
use Utopia\Validator\URL;
use Utopia\Audit\Audit;
use Utopia\Audit\Adapters\MySQL as AuditAdapter;
2019-11-09 04:27:16 +13:00
use Utopia\Locale\Locale;
2019-05-09 18:54:39 +12:00
use Auth\Auth;
use Auth\Validator\Password;
2019-07-21 23:43:06 +12:00
use Database\Database;
2020-01-04 10:00:53 +13:00
use Database\Document;
use Database\Validator\UID;
2019-05-09 18:54:39 +12:00
use Database\Validator\Authorization;
use DeviceDetector\DeviceDetector;
use GeoIp2\Database\Reader;
2020-01-04 10:00:53 +13:00
use Template\Template;
use OpenSSL\OpenSSL;
2019-05-09 18:54:39 +12:00
2019-12-17 08:35:33 +13:00
include_once __DIR__ . '/../shared/api.php';
2019-11-30 07:26:06 +13:00
2019-05-09 18:54:39 +12:00
$utopia->get('/v1/account')
->desc('Get Account')
->label('scope', 'account')
->label('sdk.namespace', 'account')
2020-01-04 10:00:53 +13:00
->label('sdk.method', 'getAccount')
2020-01-04 20:44:03 +13:00
->label('sdk.description', '/docs/references/account/get-account.md')
2019-05-09 18:54:39 +12:00
->action(
function () use ($response, &$user, $providers) {
$oauthKeys = [];
foreach ($providers as $key => $provider) {
if (!$provider['enabled']) {
continue;
}
$oauthKeys[] = 'oauth'.ucfirst($key);
$oauthKeys[] = 'oauth'.ucfirst($key).'AccessToken';
}
$response->json(array_merge($user->getArrayCopy(array_merge(
[
'$uid',
'email',
'registration',
'confirm',
'name',
],
$oauthKeys
)), ['roles' => Authorization::getRoles()]));
2019-05-09 18:54:39 +12:00
}
);
$utopia->get('/v1/account/prefs')
->desc('Get Account Preferences')
->label('scope', 'account')
->label('sdk.namespace', 'account')
2020-01-04 10:00:53 +13:00
->label('sdk.method', 'getAccountPrefs')
2020-01-04 20:44:03 +13:00
->label('sdk.description', '/docs/references/account/get-account-prefs.md')
2019-05-09 18:54:39 +12:00
->action(
function () use ($response, $user) {
$prefs = $user->getAttribute('prefs', '{}');
2019-05-09 18:54:39 +12:00
if (empty($prefs)) {
2019-08-09 16:49:36 +12:00
$prefs = '[]';
2019-05-09 18:54:39 +12:00
}
try {
$prefs = json_decode($prefs, true);
} catch (\Exception $error) {
2019-05-09 18:54:39 +12:00
throw new Exception('Failed to parse prefs', 500);
}
$response->json($prefs);
}
);
$utopia->get('/v1/account/sessions')
->desc('Get Account Active Sessions')
->label('scope', 'account')
->label('sdk.namespace', 'account')
2020-01-04 10:00:53 +13:00
->label('sdk.method', 'getAccountSessions')
2020-01-04 20:44:03 +13:00
->label('sdk.description', '/docs/references/account/get-account-sessions.md')
2019-05-09 18:54:39 +12:00
->action(
function () use ($response, $user) {
$tokens = $user->getAttribute('tokens', []);
2019-12-18 23:24:54 +13:00
$reader = new Reader(__DIR__.'/../../db/GeoLite2/GeoLite2-Country.mmdb');
$sessions = [];
$current = Auth::tokenVerify($tokens, Auth::TOKEN_TYPE_LOGIN, Auth::$secret);
$index = 0;
$countries = Locale::getText('countries');
foreach ($tokens as $token) { /* @var $token Document */
if (Auth::TOKEN_TYPE_LOGIN != $token->getAttribute('type')) {
2019-05-09 18:54:39 +12:00
continue;
}
$userAgent = (!empty($token->getAttribute('userAgent'))) ? $token->getAttribute('userAgent') : 'UNKNOWN';
2019-05-09 18:54:39 +12:00
$dd = new DeviceDetector($userAgent);
// OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
// $dd->skipBotDetection();
$dd->parse();
$sessions[$index] = [
'id' => $token->getUid(),
'OS' => $dd->getOs(),
'client' => $dd->getClient(),
'device' => $dd->getDevice(),
'brand' => $dd->getBrand(),
'model' => $dd->getModel(),
'ip' => $token->getAttribute('ip', ''),
'geo' => [],
'current' => ($current == $token->getUid()) ? true : false,
2019-05-09 18:54:39 +12:00
];
try {
$record = $reader->country($token->getAttribute('ip', ''));
$sessions[$index]['geo']['isoCode'] = strtolower($record->country->isoCode);
$sessions[$index]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown');
} catch (\Exception $e) {
2019-05-09 18:54:39 +12:00
$sessions[$index]['geo']['isoCode'] = '--';
$sessions[$index]['geo']['country'] = Locale::getText('locale.country.unknown');
}
++$index;
2019-05-09 18:54:39 +12:00
}
$response->json($sessions);
}
);
2020-01-04 20:44:03 +13:00
$utopia->get('/v1/account/logs')
2019-05-09 18:54:39 +12:00
->desc('Get Account Security Log')
->label('scope', 'account')
->label('sdk.namespace', 'account')
2020-01-04 20:44:03 +13:00
->label('sdk.method', 'getAccountLogs')
->label('sdk.description', '/docs/references/account/get-account-logs.md')
2019-05-09 18:54:39 +12:00
->action(
function () use ($response, $register, $project, $user) {
$adapter = new AuditAdapter($register->get('db'));
$adapter->setNamespace('app_'.$project->getUid());
$audit = new Audit($adapter);
$countries = Locale::getText('countries');
2019-05-09 18:54:39 +12:00
$logs = $audit->getLogsByUserAndActions($user->getUid(), [
2019-05-09 18:54:39 +12:00
'auth.register',
'auth.confirm',
'auth.login',
'auth.logout',
'auth.recovery',
'auth.recovery.reset',
'auth.oauth.login',
'auth.invite',
'auth.join',
'auth.leave',
'account.delete',
'account.update.name',
'account.update.email',
'account.update.password',
]);
2019-12-18 23:24:54 +13:00
$reader = new Reader(__DIR__.'/../../db/GeoLite2/GeoLite2-Country.mmdb');
$output = [];
2019-05-09 18:54:39 +12:00
foreach ($logs as $i => &$log) {
$log['userAgent'] = (!empty($log['userAgent'])) ? $log['userAgent'] : 'UNKNOWN';
2019-05-09 18:54:39 +12:00
$dd = new DeviceDetector($log['userAgent']);
$dd->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$dd->parse();
$output[$i] = [
'event' => $log['event'],
'ip' => $log['ip'],
'time' => strtotime($log['time']),
'OS' => $dd->getOs(),
'client' => $dd->getClient(),
'device' => $dd->getDevice(),
'brand' => $dd->getBrand(),
'model' => $dd->getModel(),
'geo' => [],
2019-05-09 18:54:39 +12:00
];
try {
$record = $reader->country($log['ip']);
$output[$i]['geo']['isoCode'] = strtolower($record->country->isoCode);
$output[$i]['geo']['country'] = $record->country->name;
$output[$i]['geo']['country'] = (isset($countries[$record->country->isoCode])) ? $countries[$record->country->isoCode] : Locale::getText('locale.country.unknown');
} catch (\Exception $e) {
2019-05-09 18:54:39 +12:00
$output[$i]['geo']['isoCode'] = '--';
$output[$i]['geo']['country'] = Locale::getText('locale.country.unknown');
2019-05-09 18:54:39 +12:00
}
}
$response->json($output);
}
);
2020-01-04 10:00:53 +13:00
$utopia->post('/v1/account')
->desc('Create a new account')
->label('webhook', 'account.create')
->label('scope', 'auth')
->label('sdk.namespace', 'account')
->label('sdk.method', 'createAccount')
2020-01-04 20:44:03 +13:00
->label('sdk.description', '/docs/references/account/create-account.md')
2020-01-04 10:00:53 +13:00
->label('abuse-limit', 10)
->param('email', '', function () { return new Email(); }, 'Account email')
->param('password', '', function () { return new Password(); }, 'User password')
->param('confirm', '', function () use ($clients) { return new Host($clients); }, 'Confirmation URL to redirect user after confirm token has been sent to user email') // TODO add our own built-in confirm page
->param('name', '', function () { return new Text(100); }, 'User name', true)
->action(
function ($email, $password, $confirm, $name) use ($request, $response, $register, $audit, $projectDB, $project, $webhook) {
if ('console' === $project->getUid()) {
$whitlistEmails = $project->getAttribute('authWhitelistEmails');
$whitlistIPs = $project->getAttribute('authWhitelistIPs');
$whitlistDomains = $project->getAttribute('authWhitelistDomains');
if (!empty($whitlistEmails) && !in_array($email, $whitlistEmails)) {
throw new Exception('Console registration is restricted to specific emails. Contact your administrator for more information.', 401);
}
if (!empty($whitlistIPs) && !in_array($request->getIP(), $whitlistIPs)) {
throw new Exception('Console registration is restricted to specific IPs. Contact your administrator for more information.', 401);
}
if (!empty($whitlistDomains) && !in_array(substr(strrchr($email, '@'), 1), $whitlistDomains)) {
throw new Exception('Console registration is restricted to specific domains. Contact your administrator for more information.', 401);
}
}
$profile = $projectDB->getCollection([ // Get user by email address
'limit' => 1,
'first' => true,
'filters' => [
'$collection='.Database::SYSTEM_COLLECTION_USERS,
'email='.$email,
],
]);
if (!empty($profile)) {
throw new Exception('Account already exists', 409);
}
$expiry = time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG;
$confirmSecret = Auth::tokenGenerator();
$loginSecret = Auth::tokenGenerator();
Authorization::disable();
$user = $projectDB->createDocument([
'$collection' => Database::SYSTEM_COLLECTION_USERS,
'$permissions' => [
'read' => ['*'],
'write' => ['user:{self}'],
],
'email' => $email,
'status' => Auth::USER_STATUS_UNACTIVATED,
'password' => Auth::passwordHash($password),
'password-update' => time(),
'registration' => time(),
'confirm' => false,
'reset' => false,
'name' => $name,
]);
Authorization::enable();
if (false === $user) {
throw new Exception('Failed saving user to DB', 500);
}
Authorization::setRole('user:'.$user->getUid());
$user
->setAttribute('tokens', new Document([
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
'$permissions' => ['read' => ['user:'.$user->getUid()], 'write' => ['user:'.$user->getUid()]],
'type' => Auth::TOKEN_TYPE_CONFIRM,
'secret' => Auth::hash($confirmSecret), // On way hash encryption to protect DB leak
'expire' => time() + Auth::TOKEN_EXPIRATION_CONFIRM,
'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'),
'ip' => $request->getIP(),
]), Document::SET_TYPE_APPEND)
->setAttribute('tokens', new Document([
'$collection' => Database::SYSTEM_COLLECTION_TOKENS,
'$permissions' => ['read' => ['user:'.$user->getUid()], 'write' => ['user:'.$user->getUid()]],
'type' => Auth::TOKEN_TYPE_LOGIN,
'secret' => Auth::hash($loginSecret), // On way hash encryption to protect DB leak
'expire' => $expiry,
'userAgent' => $request->getServer('HTTP_USER_AGENT', 'UNKNOWN'),
'ip' => $request->getIP(),
]), Document::SET_TYPE_APPEND)
;
$user = $projectDB->createDocument($user->getArrayCopy());
if (false === $user) {
throw new Exception('Failed saving tokens to DB', 500);
}
// Send email address confirmation email
$confirm = Template::parseURL($confirm);
$confirm['query'] = Template::mergeQuery(((isset($confirm['query'])) ? $confirm['query'] : ''), ['userId' => $user->getUid(), 'token' => $confirmSecret]);
$confirm = Template::unParseURL($confirm);
$body = new Template(__DIR__.'/../../config/locales/templates/'.Locale::getText('auth.emails.confirm.body'));
$body
->setParam('{{direction}}', Locale::getText('settings.direction'))
->setParam('{{project}}', $project->getAttribute('name', ['[APP-NAME]']))
->setParam('{{name}}', $name)
->setParam('{{redirect}}', $confirm)
;
$mail = $register->get('smtp'); /* @var $mail \PHPMailer\PHPMailer\PHPMailer */
$mail->addAddress($email, $name);
$mail->Subject = Locale::getText('auth.emails.confirm.title');
$mail->Body = $body->render();
$mail->AltBody = strip_tags($body->render());
try {
$mail->send();
} catch (\Exception $error) {
// if($failure) {
// $response->redirect($failure);
// return;
// }
// throw new Exception('Problem sending mail: ' . $error->getMessage(), 500);
}
$webhook
->setParam('payload', [
'name' => $name,
'email' => $email,
])
;
$audit
->setParam('userId', $user->getUid())
->setParam('event', 'account.create')
;
$response
->addCookie(Auth::$cookieName, Auth::encodeSession($user->getUid(), $loginSecret), $expiry, '/', COOKIE_DOMAIN, ('https' == $request->getServer('REQUEST_SCHEME', 'https')), true, COOKIE_SAMESITE);
$response->json(array('result' => 'success'));
}
);
2019-05-09 18:54:39 +12:00
$utopia->patch('/v1/account/name')
->desc('Update Account Name')
2020-01-04 20:44:03 +13:00
->label('webhook', 'account.update-account-name')
2019-05-09 18:54:39 +12:00
->label('scope', 'account')
->label('sdk.namespace', 'account')
2020-01-04 20:44:03 +13:00
->label('sdk.method', 'updateAccountName')
->label('sdk.description', '/docs/references/account/update-account-name.md')
->param('name', '', function () { return new Text(100); }, 'User name')
2019-05-09 18:54:39 +12:00
->action(
function ($name) use ($response, $user, $projectDB, $audit) {
2019-05-09 18:54:39 +12:00
$user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [
'name' => $name,
]));
if (false === $user) {
2019-05-09 18:54:39 +12:00
throw new Exception('Failed saving user to DB', 500);
}
$audit->setParam('event', 'account.update.name');
$response->json(array('result' => 'success'));
}
);
$utopia->patch('/v1/account/password')
->desc('Update Account Password')
2020-01-04 20:44:03 +13:00
->label('webhook', 'account.update-account-password')
2019-05-09 18:54:39 +12:00
->label('scope', 'account')
->label('sdk.namespace', 'account')
2020-01-04 20:44:03 +13:00
->label('sdk.method', 'updateAccountPassword')
->label('sdk.description', '/docs/references/account/update-account-password.md')
->param('password', '', function () { return new Password(); }, 'New password')
->param('old-password', '', function () { return new Password(); }, 'Old password')
2019-05-09 18:54:39 +12:00
->action(
function ($password, $oldPassword) use ($response, $user, $projectDB, $audit) {
if (!Auth::passwordVerify($oldPassword, $user->getAttribute('password'))) { // Double check user password
2019-05-09 18:54:39 +12:00
throw new Exception('Invalid credentials', 401);
}
$user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [
'password' => Auth::passwordHash($password),
]));
if (false === $user) {
2019-05-09 18:54:39 +12:00
throw new Exception('Failed saving user to DB', 500);
}
$audit->setParam('event', 'account.update.password');
$response->json(array('result' => 'success'));
}
);
$utopia->patch('/v1/account/email')
->desc('Update Account Email')
->label('webhook', 'account.update-email')
->label('scope', 'account')
->label('sdk.namespace', 'account')
->label('sdk.method', 'updateEmail')
2019-10-08 20:21:54 +13:00
->label('sdk.description', '/docs/references/account/update-email.md')
->param('email', '', function () { return new Email(); }, 'Email Address')
->param('password', '', function () { return new Password(); }, 'User Password')
2019-05-09 18:54:39 +12:00
->action(
function ($email, $password) use ($response, $user, $projectDB, $audit) {
if (!Auth::passwordVerify($password, $user->getAttribute('password'))) { // Double check user password
2019-05-09 18:54:39 +12:00
throw new Exception('Invalid credentials', 401);
}
2019-07-21 23:43:06 +12:00
$profile = $projectDB->getCollection([ // Get user by email address
'limit' => 1,
'first' => true,
'filters' => [
'$collection='.Database::SYSTEM_COLLECTION_USERS,
'email='.$email,
],
2019-07-21 23:43:06 +12:00
]);
if (!empty($profile)) {
2019-07-21 23:43:06 +12:00
throw new Exception('User already registered', 400);
}
2019-05-09 18:54:39 +12:00
// TODO after this user needs to confirm mail again
$user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [
'email' => $email,
]));
if (false === $user) {
2019-05-09 18:54:39 +12:00
throw new Exception('Failed saving user to DB', 500);
}
$audit->setParam('event', 'account.update.email');
$response->json(array('result' => 'success'));
}
);
$utopia->patch('/v1/account/prefs')
->desc('Update Account Prefs')
->label('webhook', 'account')
->label('scope', 'account')
->label('sdk.namespace', 'account')
->label('sdk.method', 'updatePrefs')
->param('prefs', '', function () { return new \Utopia\Validator\Mock();}, 'Prefs key-value JSON object string.')
2019-10-08 20:21:54 +13:00
->label('sdk.description', '/docs/references/account/update-prefs.md')
2019-05-09 18:54:39 +12:00
->action(
function ($prefs) use ($response, $user, $projectDB, $audit) {
2019-05-09 18:54:39 +12:00
$user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [
'prefs' => json_encode(array_merge(json_decode($user->getAttribute('prefs', '{}'), true), $prefs)),
]));
if (false === $user) {
2019-05-09 18:54:39 +12:00
throw new Exception('Failed saving user to DB', 500);
}
$audit->setParam('event', 'account.update.prefs');
$response->json(array('result' => 'success'));
}
);
$utopia->delete('/v1/account')
->desc('Delete Account')
->label('webhook', 'account.delete')
->label('scope', 'account')
->label('sdk.namespace', 'account')
->label('sdk.method', 'delete')
2019-10-08 20:21:54 +13:00
->label('sdk.description', '/docs/references/account/delete.md')
2019-05-09 18:54:39 +12:00
->action(
function () use ($response, $request, $user, $projectDB, $audit) {
2019-05-09 18:54:39 +12:00
$user = $projectDB->updateDocument(array_merge($user->getArrayCopy(), [
'status' => Auth::USER_STATUS_BLOCKED,
]));
if (false === $user) {
2019-05-09 18:54:39 +12:00
throw new Exception('Failed saving user to DB', 500);
}
//TODO delete all tokens or only current session?
//TODO delete all user data according to GDPR. Make sure everything is backed up and backups are deleted later
/*
2019-05-09 18:54:39 +12:00
* Data to delete
* * Tokens
* * Memberships
*/
$audit
->setParam('event', 'account.delete')
->setParam('data', $user->getArrayCopy())
;
$response
->addCookie(Auth::$cookieName, '', time() - 3600, '/', COOKIE_DOMAIN, ('https' == $request->getServer('REQUEST_SCHEME', 'https')), true, COOKIE_SAMESITE)
2019-05-09 18:54:39 +12:00
->json(array('result' => 'success'));
}
);