public function adm_login($args) {
		$this->init();
		$logger = $this->logger;

		if ($this->store_is_logged_in())
			return [AdminStoreError::USER_ALREADY_LOGGED_IN];

		if (!isset($args['post']))
			return [AdminStoreError::DATA_INCOMPLETE];
		if (!Common::check_idict($args['post'], ['uname', 'upass']))
			return [AdminStoreError::DATA_INCOMPLETE];
		extract($args['post']);

		$usalt = $this->store->query(
			"SELECT usalt FROM udata WHERE uname=? LIMIT 1",
			[$uname]);
		if (!$usalt)
			# user not found
			return [AdminStoreError::USER_NOT_FOUND];
		$usalt = $usalt['usalt'];

		$udata = $this->store_match_password($uname, $upass, $usalt);
		if (!$udata) {
			# wrong password
			$logger->warning(
				"Zapmin: login: wrong password: '$uname'.");
			return [AdminStoreError::WRONG_PASSWORD];
		}

		// generate token
		$token = $this->generate_secret(
			$upass . $usalt . time(), $usalt);

		$sid = $this->store->insert('usess', [
			'uid'   => $udata['uid'],
			'token' => $token,
		], 'sid');
		if ($this->dbtype == 'mysql') {
			// mysql has no parametrized default values, and can't
			// invoke trigger on currently inserted table
			$expire_at = $this->store->stmt_fragment('datetime', [
				'delta' => $this->expiration,
			]);
			$this->store->query_raw(sprintf(
				"UPDATE usess SET expire=(%s) WHERE sid='%s'",
				$expire_at, $sid));
		}

		// token must be used by the router; this is a subset
		// of return value of get_safe_user_data() so it needs
		// a re-request after signing in
		$logger->info("Zapmin: login: OK: '$uname'.");
		return [0, [
			'uid' => $udata['uid'],
			'uname' => $udata['uname'],
			'token' => $token,
		]];
	}