1. Go to this page and download the library: Download tobento/app-user-web library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
tobento / app-user-web example snippets
use Tobento\App\AppFactory;
// Create the app
$app = (new AppFactory())->createApp();
// Add directories:
$app->dirs()
->dir(realpath(__DIR__.'/../'), 'root')
->dir(realpath(__DIR__.'/../app/'), 'app')
->dir($app->dir('app').'config', 'config', group: 'config')
->dir($app->dir('root').'public', 'public')
->dir($app->dir('root').'vendor', 'vendor');
// Adding boots
$app->boot(\Tobento\App\User\Web\Boot\UserWeb::class);
// Run the app
$app->run();
'features' => [
new Feature\Home(),
// Or:
new Feature\Home(
// The view to render:
view: 'user/home',
// A menu name to show the home link or null if none.
menu: 'main',
menuLabel: 'Home',
// A menu parent name (e.g. 'user') or null if none.
menuParent: null,
// If true, routes are being localized.
localizeRoute: false,
),
],
use Tobento\App\RateLimiter\Symfony\Registry\SlidingWindow;
'features' => [
new Feature\Login(),
// Or:
new Feature\Login(
// The view to render:
view: 'user/login',
// A menu name to show the login link or null if none.
menu: 'header',
menuLabel: 'Log in',
// A menu parent name (e.g. 'user') or null if none.
menuParent: null,
// Specify a rate limiter:
rateLimiter: new SlidingWindow(limit: 10, interval: '5 Minutes'),
// see: https://github.com/tobento-ch/app-rate-limiter#available-rate-limiter-registries
// Specify the identity attributes to be checked on login.
identifyBy: ['email', 'username', 'smartphone', 'password'],
// You may set a user verifier(s), see: https://github.com/tobento-ch/app-user#user-verifier
/*userVerifier: function() {
return new \Tobento\App\User\Authenticator\UserRoleVerifier('editor', 'author');
},*/
// The period of time from the present after which the auth token MUST be considered expired.
expiresAfter: 1500, // int|\DateInterval
// If you want to support remember. If set and the user wants to be remembered,
// this value replaces the expiresAfter parameter.
remember: new \DateInterval('P6M'), // null|int|\DateInterval
// The message and redirect route if a user is authenticated.
authenticatedMessage: 'You are logged in!',
authenticatedRedirectRoute: 'home', // or null (no redirection)
// The message shown if a login attempt fails.
failedMessage: 'Invalid user or password.',
// The redirect route after a successful login.
successRoute: 'home',
// The message shown after a user successfully log in.
successMessage: 'Welcome back :greeting.', // or null
// If set, it shows the forgot password link. Make sure the Feature\ForgotPassword is set too.
forgotPasswordRoute: 'forgot-password.identity', // or null
// The two factor authentication route.
twoFactorRoute: 'twofactor.code.show',
// If true, routes are being localized.
localizeRoute: false,
),
],
'features' => [
new Feature\Login(
// If you want to support remember. If set and the user wants to be remembered,
// this value replaces the expiresAfter parameter.
remember: new \DateInterval('P6M'), //null|int|\DateInterval
),
],
use Tobento\App\User;
'middlewares' => [
// You may uncomment it and set it on each route individually
// using the User\Middleware\AuthenticationWith::class!
User\Middleware\Authentication::class,
User\Middleware\User::class,
[User\Web\Middleware\RememberedToken::class, 'isRememberedAfter' => 1500],
// or with date interval:
[User\Web\Middleware\RememberedToken::class, 'isRememberedAfter' => new \DateInterval('PT2H')],
],
'features' => [
new Feature\TwoFactorAuthenticationCode(),
// Or:
new Feature\TwoFactorAuthenticationCode(
// The view to render:
view: 'user/twofactor-code',
// The period of time from the present after which the verification code MUST be considered expired.
codeExpiresAfter: 300, // int|\DateInterval
// The seconds after a new code can be reissued.
canReissueCodeAfter: 60,
// The message and redirect route if a user is unauthenticated.
unauthenticatedMessage: 'You have insufficient rights to access the requested resource!',
unauthenticatedRedirectRoute: 'home', // or null (no redirection)
// The redirect route after a successful code verification.
successRoute: 'home',
// The message shown after a successful code verification.
successMessage: 'Welcome back :greeting.', // or null
// If true, routes are being localized.
localizeRoute: false,
),
],
use Tobento\App\User\Web\Feature\Login;
use Tobento\App\User\UserInterface;
class CustomLoginFeature extends Login
{
/**
* Returns true if the user is our conditions here:
if (in_array($user->getRoleKey(), ['business'])) {
return true;
}
return false;
}
}
'features' => [
new CustomLoginFeature(
//...
),
],
use Tobento\App\User\Authentication\Token\TokenInterface;
use Tobento\App\User\Authenticator\TokenAuthenticator;
use Tobento\App\User\Authenticator\TokenVerifierInterface;
use Tobento\App\User\Exception\AuthenticationException;
use Tobento\App\User\UserInterface;
use Tobento\App\User\UserRepositoryInterface;
use Tobento\Service\Acl\AclInterface;
class CustomTokenAuthenticator extends TokenAuthenticator
{
public function __construct(
protected AclInterface $acl,
protected UserRepositoryInterface $userRepository,
protected null|TokenVerifierInterface $tokenVerifier = null,
) {}
/**
* Authenticate token.
*
* @param TokenInterface $token
* @return UserInterface
* @throws AuthenticationException If authentication fails.
*/
public function authenticate(TokenInterface $token): UserInterface
{
$user = parent::authenticate($token);
if ($token->authenticatedVia() === 'loginform-twofactor') {
$role = $this->acl->getRole('registered');
if (is_null($role)) {
throw new AuthenticationException('Registered role not set up');
}
$user->setRole($role);
$user->setRoleKey($role->key());
$user->setPermissions([]); // clear user specific permissions too.
}
return $user;
}
}
'features' => [
new Feature\Logout(),
// Or:
new Feature\Logout(
// A menu name to show the logout link or null if none.
menu: 'header',
menuLabel: 'Log out',
// A menu parent name (e.g. 'user') or null if none.
menuParent: null,
// The redirect route after a successful logout.
redirectRoute: 'home',
// The message and redirect route if a user is unauthenticated.
unauthenticatedMessage: 'You have insufficient rights to access the requested resource!',
unauthenticatedRedirectRoute: 'home', // or null (no redirection)
// If true, routes are being localized.
localizeRoute: false,
),
],
'features' => [
new Feature\ForgotPassword(),
// Or:
new Feature\ForgotPassword(
viewIdentity: 'user/forgot-password/identity',
viewReset: 'user/forgot-password/reset',
// Specify the identity attributes to be checked on identity.
identifyBy: ['email', 'username', 'smartphone'],
// You may set a user verifier(s), see: https://github.com/tobento-ch/app-user#user-verifier
/*userVerifier: function() {
return new \Tobento\App\User\Authenticator\UserRoleVerifier('editor', 'author');
},*/
// The period of time from the present after which the verification token MUST be considered expired.
tokenExpiresAfter: 300, // int|\DateInterval
// The seconds after a new token can be reissued.
canReissueTokenAfter: 60,
// The message shown if an identity attempt fails.
identityFailedMessage: 'Invalid name or user.',
// The message and redirect route if a user is authenticated.
authenticatedMessage: 'You are logged in!',
authenticatedRedirectRoute: 'home', // or null (no redirection)
// The redirect route after a successful password reset.
successRedirectRoute: 'home',
// The message shown after a successful password reset.
successMessage: 'Your password has been reset!', // or null
// If true, routes are being localized.
localizeRoute: false,
),
],
use Tobento\App\User\Web\Feature\ForgotPassword;
use Tobento\App\User\Web\TokenInterface;
use Tobento\App\User\Web\Notification;
use Tobento\App\User\UserInterface;
use Tobento\Service\Notifier\NotifierInterface;
use Tobento\Service\Notifier\UserRecipient;
use Tobento\Service\Routing\RouterInterface;
class CustomForgotPasswordFeature extends ForgotPassword
{
protected function sendLinkNotification(
TokenInterface $token,
UserInterface $user,
NotifierInterface $notifier,
RouterInterface $router,
): void {
$notification = new Notification\ResetPassword(
token: $token,
url: (string)$router->url('forgot-password.reset', ['token' => $token->id()]),
);
// The receiver of the notification:
$recipient = new UserRecipient(user: $user);
// Send the notification to the recipient:
$notifier->send($notification, $recipient);
}
}
'features' => [
new CustomForgotPasswordFeature(),
],
'features' => [
new Feature\Register(),
// Or:
new Feature\Register(
// The view to render:
view: 'user/register',
// A menu name to show the register link or null if none.
menu: 'header',
menuLabel: 'Register',
// A menu parent name (e.g. 'user') or null if none.
menuParent: null,
// The message and redirect route if a user is authenticated.
authenticatedMessage: 'You are logged in!',
authenticatedRedirectRoute: 'home',
// The default role key for the registered user.
roleKey: 'registered',
// The redirect route after a successful registration.
successRedirectRoute: 'login',
// You may redirect to the verification account page
// see: https://github.com/tobento-ch/app-user-web#account-verification
// If true, user has the option to subscribe to the newsletter.
newsletter: false,
// If a terms route is specified, users need to agree terms and conditions.
termsRoute: null,
/*termsRoute: static function (RouterInterface $router): string {
return (string)$router->url('blog.show', ['key' => 'terms']);
},*/
// If true, routes are being localized.
localizeRoute: false,
),
],
use Tobento\App\User\Web\Feature\Register;
class CustomRegisterFeature extends Register
{
protected function validationRules(): array
{
return [
'user_type' => 'string',
'address.fistname' => '
'features' => [
new CustomRegisterFeature(
// specify your custom view if (step 1.A):
view: 'user/custom-register',
// No need to change the default view for (step 1.B)
view: 'user/register',
//...
),
],
use Tobento\App\User\Web\Feature\Register;
use Tobento\Service\Acl\AclInterface;
use Tobento\Service\Validation\ValidationInterface;
class CustomRegisterFeature extends Register
{
protected function determineRoleKey(AclInterface $acl, ValidationInterface $validation): string
{
return match ($validation->valid()->get('user_type', '')) {
'business' => $acl->hasRole('business') ? 'business' : $this->roleKey,
default => $this->roleKey,
};
}
}
'features' => [
new CustomRegisterFeature(
//...
),
],
use Tobento\App\User\Web\Listener\AutoLoginAfterRegistration;
'listeners' => [
\Tobento\App\User\Web\Event\Registered::class => [
AutoLoginAfterRegistration::class,
// Or you may set the expires after:
[AutoLoginAfterRegistration::class, ['expiresAfter' => 1500]], // in seconds
[AutoLoginAfterRegistration::class, ['expiresAfter' => new \DateInterval('PT1H')]],
],
],
'features' => [
new Feature\Register(
// redirect users to the profile page
// after successful registration:
successRedirectRoute: 'profile.edit',
),
],
'features' => [
new Feature\Register(
// redirect users to the verification account page
// after successful registration:
successRedirectRoute: 'verification.account',
),
// make sure the verification feature is set:
Feature\Verification::class,
],
use Tobento\App\User\Web\Feature\Profile;
use Tobento\App\AppInterface;
use Tobento\App\User\Middleware\Authenticated;
use Tobento\App\User\Middleware\Verified;
class CustomProfileFeature extends Profile
{
protected function configureMiddlewares(AppInterface $app): array
{
return [
// The Authenticated::class middleware protects routes from unauthenticated users:
[
Authenticated::class,
// you may specify a custom message to show to the user:
'message' => $this->unauthenticatedMessage,
// you may specify a message level:
//'messageLevel' => 'notice',
// you may specify a route name for redirection:
'redirectRoute' => $this->unauthenticatedRedirectRoute,
],
// The Verified::class middleware protects routes from unverified users:
[
Verified::class,
// you may specify a custom message to show to the user:
'message' => 'You have insufficient rights to access the requested resource!',
// you may specify a message level:
'messageLevel' => 'notice',
// you may specify a route name for redirection:
'redirectRoute' => 'verification.account',
],
];
}
}
'features' => [
new CustomProfileFeature(
//...
),
],
use Tobento\App\User\Web\Feature\Register;
use Tobento\App\User\UserInterface;
class CustomRegisterFeature extends Register
{
protected function configureSuccessRedirectRoute(UserInterface $user): string
{
if (in_array($user->getRoleKey(), ['business'])) {
return 'verification.account';
}
return 'login';
}
}
'features' => [
new Feature\Register(
// If a terms route is specified, users need to agree terms and conditions.
termsRoute: 'your.terms.route.name',
// Or you may use router directly:
termsRoute: static function (RouterInterface $router): string {
return (string)$router->url('blog.show', ['key' => 'terms']);
},
),
],
use Tobento\App\Spam\Factory;
'detectors' => [
'register' => new Factory\Composite(
new Factory\Honeypot(inputName: 'hp'),
new Factory\MinTimePassed(inputName: 'mtp', milliseconds: 1000),
),
]
'features' => [
new Feature\Notifications(),
// Or:
new Feature\Notifications(
// The view to render:
view: 'user/notifications',
// The notifier storage channel used to retrieve notifications.
notifierStorageChannel: 'storage',
// A menu name to show the notifications link or null if none.
menu: 'main',
menuLabel: 'Notifications',
// A menu parent name (e.g. 'user') or null if none.
menuParent: null,
// The message and redirect route if a user is unauthenticated.
unauthenticatedMessage: 'You have insufficient rights to access the requested resource!',
unauthenticatedRedirectRoute: 'login', // or null (no redirection)
// If true, routes are being localized.
localizeRoute: false,
),
],
use Tobento\Service\Notifier\NotifierInterface;
use Tobento\Service\Notifier\Notification;
use Tobento\Service\Notifier\Recipient;
class SomeService
{
public function send(NotifierInterface $notifier): void
{
// Create a Notification that has to be sent:
// using the "email" and "sms" channel
$notification = new Notification(
subject: 'New Invoice',
content: 'You got a new invoice for 15 EUR.',
channels: ['mail', 'sms', 'storage'],
);
// with specific storage message. Will be displayed on the notifications page:
$notification->addMessage('storage', new Message\Storage([
'message' => 'You received a new order.',
'action_text' => 'View Order',
'action_route' => 'orders.view',
'action_route_parameters' => ['id' => 55],
]));
// The receiver of the notification:
$recipient = new Recipient(
email: '[email protected]',
phone: '15556666666',
id: 'unique-user-id',
);
// Send the notification to the recipient:
$notifier->send($notification, $recipient);
}
}
use Tobento\App\AppInterface;
use Tobento\App\User\Web\Feature\Notifications;
use Tobento\Service\Notifier\ChannelsInterface;
use Tobento\Service\Notifier\Storage;
use Psr\SimpleCache\CacheInterface;
class CustomNotificationsFeature extends Notifications
{
/**
* Returns the user's unread notifications count for the menu badge.
*
* @param ChannelsInterface $channels
* @param UserInterface $user
* @param AppInterface $app
* @return int
*/
protected function getUnreadNotificationsCount(
ChannelsInterface $channels,
UserInterface $user,
AppInterface $app,
): int {
$channel = $channels->get(name: $this->notifierStorageChannel);
if (!$channel instanceof Storage\Channel) {
return 0;
}
$key = sprintf('unread_notifications_count:%s', (string)$user->id());
$cache = $app->get(CacheInterface::class);
if ($cache->has($key)) {
return $cache->get($key);
}
$count = $channel->repository()->count(where: [
'recipient_id' => $user->id(),
'read_at' => ['null'],
]);
$cache->set($key, $count, 60);
return $count;
}
}
'features' => [
new CustomNotificationsFeature(
//...
),
],
use Tobento\Service\Schedule\Task;
use Butschster\CronExpression\Generator;
$schedule->task(
(new Task\CommandTask(
command: 'notifications:clear --read-only',
))
// schedule task:
->cron(Generator::create()->weekly())
);
'features' => [
new Feature\Profile(),
// Or:
new Feature\Profile(
// The view to render:
view: 'user/profile/edit',
// A menu name to show the profile link or null if none.
menu: 'main',
menuLabel: 'Profile',
// A menu parent name (e.g. 'user') or null if none.
menuParent: null,
// The message and redirect route if a user is unauthenticated.
unauthenticatedMessage: 'You have insufficient rights to access the requested resource!',
unauthenticatedRedirectRoute: 'login', // or null (no redirection)
// If true, it displays the channel verification section to verify channels.
// Make sure the Verification Feature is enabled.
channelVerifications: true,
// The redirect route after a successfully account deletion.
successDeleteRedirectRoute: 'home',
// If true, routes are being localized.
localizeRoute: false,
),
],
use Tobento\App\User\Web\Feature\Profile;
use Tobento\App\User\UserInterface;
class CustomProfileFeature extends Profile
{
/**
* Returns the validation rules for updating the user's profile information.
*
* @param UserInterface $user
* @return array
*/
protected function validationUpdateRules(UserInterface $user): array
{
// your rules:
$rules = [
'user_type' => 'string',
'address.fistname' => '
'features' => [
new CustomProfileFeature(
// specify your custom view if (step 1.A):
view: 'user/profile/custom-edit',
// No need to change the default view for (step 1.B)
view: 'user/profile/edit',
//...
),
],
use Tobento\App\User\Web\Feature\Profile;
use Tobento\App\User\UserInterface;
use Tobento\App\Notifier\AvailableChannelsInterface;
class CustomProfileFeature extends Profile
{
/**
* Configure the available verification channels to display.
*
* @param AvailableChannelsInterface $channels
* @param UserInterface $user
* @return AvailableChannelsInterface
*/
protected function configureAvailableChannels(
AvailableChannelsInterface $channels,
UserInterface $user,
): AvailableChannelsInterface {
if (! $this->channelVerifications) {
// do not display any at all:
return $channels->only([]);
}
return $channels->only(['mail', 'sms']);
}
}
'features' => [
new CustomProfileFeature(
//...
),
],
'features' => [
new Feature\ProfileSettings(),
// Or:
new Feature\ProfileSettings(
// The view to render:
view: 'user/profile/settings',
// A menu name to show the profile settings link or null if none.
menu: 'main',
menuLabel: 'Profile Settings',
// A menu parent name (e.g. 'user') or null if none.
menuParent: null,
// The message and redirect route if a user is unauthenticated.
unauthenticatedMessage: 'You have insufficient rights to access the requested resource!',
unauthenticatedRedirectRoute: 'login', // or null (no redirection)
// If true, routes are being localized.
localizeRoute: false,
),
],
use Tobento\App\User\Web\Feature\ProfileSettings;
use Tobento\App\User\UserInterface;
class CustomProfileSettingsFeature extends ProfileSettings
{
/**
* Returns the validation rules for updating the user's profile settings.
*
* @param UserInterface $user
* @return array
*/
protected function validationRules(UserInterface $user): array
{
// your rules:
$rules = [
'settings.something' => '
'features' => [
new CustomProfileSettingsFeature(
// specify your custom view if (step 1.A):
view: 'user/profile/custom-settings',
// No need to change the default view for (step 1.B)
view: 'user/profile/settings',
//...
),
],
use Tobento\App\User\Web\Feature\ProfileSettings;
use Tobento\App\User\UserInterface;
use Tobento\App\Notifier\AvailableChannelsInterface;
class CustomProfileSettingsFeature extends ProfileSettings
{
/**
* Configure the available channels.
*
* @param AvailableChannelsInterface $channels
* @param UserInterface $user
* @return AvailableChannelsInterface
*/
protected function configureAvailableChannels(
AvailableChannelsInterface $channels,
UserInterface $user,
): AvailableChannelsInterface {
return $channels
->only(['mail', 'sms', 'storage'])
->withTitle('storage', 'Account')
->sortByTitle();
// Or you may return no channels at all:
return $channels->only([]);
}
}
'features' => [
new CustomProfileSettingsFeature(
//...
),
],
'features' => [
new Feature\Verification(),
// Or:
new Feature\Verification(
// The view to render:
viewAccount: 'user/verification/account',
viewChannel: 'user/verification/channel',
// The period of time from the present after which the verification code MUST be considered expired.
codeExpiresAfter: 300, // int|\DateInterval
// The seconds after a new code can be reissued.
canReissueCodeAfter: 60,
// The message and redirect route if a user is unauthenticated.
unauthenticatedMessage: 'You have insufficient rights to access the requested resource!',
unauthenticatedRedirectRoute: 'login', // or null (no redirection)
// The redirect route after a verified channel.
verifiedRedirectRoute: 'home',
// If true, routes are being localized.
localizeRoute: false,
),
],
use Tobento\App\User\Web\Feature\Verification;
use Tobento\App\User\UserInterface;
use Tobento\App\Notifier\AvailableChannelsInterface;
class CustomVerificationFeature extends Verification
{
/**
* Configure the available channels.
*
* @param AvailableChannelsInterface $channels
* @param UserInterface $user
* @return AvailableChannelsInterface
*/
protected function configureAvailableChannels(
AvailableChannelsInterface $channels,
UserInterface $user,
): AvailableChannelsInterface {
//return $channels->only(['mail', 'sms']); // default
return $channels->only(['mail', 'sms', 'chat/slack']);
}
/**
* Returns the notifier channel for the given verification channel.
*
* @param string $channel
* @return null|string
*/
protected function getNotifierChannelFor(string $channel): null|string
{
return match ($channel) {
'email' => 'mail',
'smartphone' => 'sms',
'slack' => 'chat/slack',
default => null,
};
}
/**
* Determine if the channel can be verified.
*
* @param string $channel
* @param AvailableChannelsInterface $channels
* @param null|UserInterface $user
* @return bool
*/
protected function canVerifyChannel(string $channel, AvailableChannelsInterface $channels, null|UserInterface $user): bool
{
if (is_null($user) || !$user->isAuthenticated()) {
return false;
}
if (! $channels->has((string)$this->getNotifierChannelFor($channel))) {
return false;
}
return match ($channel) {
'email' => !empty($user->email()) && ! $user->isVerified([$channel]),
'smartphone' => !empty($user->smartphone()) && ! $user->isVerified([$channel]),
'slack' => !empty($user->setting('slack')) && ! $user->isVerified([$channel]),
default => false,
};
}
}
'features' => [
new CustomVerificationFeature(
//...
),
],
use Tobento\Service\Schedule\Task;
use Butschster\CronExpression\Generator;
$schedule->task(
(new Task\CommandTask(
command: 'user-web:clear-tokens',
))
// schedule task:
->cron(Generator::create()->weekly())
);
use Tobento\Service\Schedule\Task;
use Butschster\CronExpression\Generator;
$schedule->task(
(new Task\CommandTask(
command: 'auth:purge-tokens',
))
// schedule task:
->cron(Generator::create()->weekly())
);
use Tobento\Service\Acl\AclInterface;
var_dump($view->acl() instanceof AclInterface);
// bool(true)
if ($view->acl()->can('comments.write')) {
<div>Only users with the permission "comments.write" can see this!</div>
}
use Tobento\App\User\Web\Event;
use Tobento\App\User\Web\Event;
class UserNewsletterSubscriber
{
public function subscribe(Event\Registered $event): void
{
if ($event->user()->newsletter()) {
// subscribe...
}
}
public function resubscribe(Event\UpdatedProfile $event): void
{
if ($event->user()->email() !== $event->oldUser()->email()) {
// unsubscribe user with the old email address...
// subscribe user with the new email address...
}
}
public function subscribeOrUnsubscribe(Event\UpdatedProfileSettings $event): void
{
if ($event->user()->newsletter()) {
// subscribe...
} else {
// unsubscribe...
}
}
public function unsubscribe(Event\DeletedAccount $event): void
{
// just unsubscribe...
}
}
use Tobento\App\User\Web\Notification;
use Tobento\App\User\Web\PinCodeVerificator;
use Tobento\App\User\Web\TokenInterface;
use Tobento\App\User\UserInterface;
use Tobento\Service\Notifier\NotifierInterface;
use Tobento\Service\Routing\RouterInterface;
class CustomPinCodeVerificator extends PinCodeVerificator
{
protected function createNotification(
TokenInterface $token,
UserInterface $user,
array $channels,
): NotificationInterface {
return (new Notification\VerificationCode(token: $token))->channels($channels);
}
}
use Tobento\App\User\Web;
'interfaces' => [
Web\PinCodeVerificatorInterface::class => CustomPinCodeVerificator::class,
],
'features' => [
new Feature\Home(
localizeRoute: true,
),
new Feature\Register(
localizeRoute: true,
),
],
app/config/user_web.php
app/config/user_web.php
app/config/user.php
config/user_web.php
config/user.php
views/user/
custom-register.php
config/event.php
config/user_web.php
config/event.php
config/user_web.php
config/event.php
config/user_web.php
app/config/spam.php
views/user/
profile/custom-edit.php
views/user/
profile/custom-settings.php
app/config/notifier.php
php ap user-web:clear-tokens
php ap auth:purge-tokens
app/config/notifier.php
config/event.php
app/config/language.php
en
Loading please wait ...
Before you can download the PHP files, the dependencies should be resolved. This can take some minutes. Please be patient.