- Worked th error layout
- Added some picto for the error views - Bugfixes
This commit is contained in:
		
							parent
							
								
									421024f260
								
							
						
					
					
						commit
						fd693fc8fc
					
				@ -5,5 +5,12 @@
 | 
				
			|||||||
    "subtitle": "You may have followed an invalid or expired link...",
 | 
					    "subtitle": "You may have followed an invalid or expired link...",
 | 
				
			||||||
    "message": "It seems like an error occured on the link you visited / action you tried. A notification was sent to the application's developer.",
 | 
					    "message": "It seems like an error occured on the link you visited / action you tried. A notification was sent to the application's developer.",
 | 
				
			||||||
    "back": "Return to previous page"
 | 
					    "back": "Return to previous page"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "500": {
 | 
				
			||||||
 | 
					    "title": "An error occured",
 | 
				
			||||||
 | 
					    "page-title": "500 - An error occured",
 | 
				
			||||||
 | 
					    "subtitle": "L'action que vous avez tenté d'effectué semble avoir échoué.",
 | 
				
			||||||
 | 
					    "message": "Un message d'erreur a été envoyé au développeur de cette application.",
 | 
				
			||||||
 | 
					    "back": "Revenir à la page précédente"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1,4 +1,11 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
 | 
					  "401": {
 | 
				
			||||||
 | 
					    "title": "Accès refusé",
 | 
				
			||||||
 | 
					    "page-title": "401 - Accès refusé ",
 | 
				
			||||||
 | 
					    "subtitle": "Vous n'avez pas les droits nécessaire pour consulter ce lien...",
 | 
				
			||||||
 | 
					    "message": "Vous avez tentez d'accéder un lien qui ne vous est pas permis ou dont la permission ne vous a pas été accordé.",
 | 
				
			||||||
 | 
					    "back": "Revenir à la page précédente"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  "404": {
 | 
					  "404": {
 | 
				
			||||||
    "title": "Page introuvable",
 | 
					    "title": "Page introuvable",
 | 
				
			||||||
    "page-title": "404 - Page introuvable",
 | 
					    "page-title": "404 - Page introuvable",
 | 
				
			||||||
 | 
				
			|||||||
@ -24,7 +24,7 @@ return [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    SecurityHandler::class => create(SecurityHandler::class)->constructor(function() {
 | 
					    SecurityHandler::class => create(SecurityHandler::class)->constructor(function() {
 | 
				
			||||||
        return new RedirectResponse(getenv("URL_BASE")."/connexion");
 | 
					        return new RedirectResponse(getenv("URL_BASE")."/connexion");
 | 
				
			||||||
    }),
 | 
					    }, get('authentication.unauthorize')),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    'authentication.error' => function($c, Picea $picea) {
 | 
					    'authentication.error' => function($c, Picea $picea) {
 | 
				
			||||||
        return function($message) use ($picea) {
 | 
					        return function($message) use ($picea) {
 | 
				
			||||||
@ -36,6 +36,16 @@ return [
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    'authentication.unauthorize' => function($c, Picea $picea) {
 | 
				
			||||||
 | 
					        return function($message) use ($picea) {
 | 
				
			||||||
 | 
					            return new HtmlResponse($picea->renderHtml('lean/error/401', [
 | 
				
			||||||
 | 
					                'title' => "",
 | 
				
			||||||
 | 
					                'subtitle' => "",
 | 
				
			||||||
 | 
					                'message' => $message,
 | 
				
			||||||
 | 
					            ]));
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    EmailConfiguration::class => function($c) {
 | 
					    EmailConfiguration::class => function($c) {
 | 
				
			||||||
        $email = new EmailConfiguration( EmailConfiguration::AUTH_TYPE_SMTP );
 | 
					        $email = new EmailConfiguration( EmailConfiguration::AUTH_TYPE_SMTP );
 | 
				
			||||||
        $email->smtpHost = getenv('SMTP_HOST');
 | 
					        $email->smtpHost = getenv('SMTP_HOST');
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										0
									
								
								skeleton/var/cache/.gitkeep
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								skeleton/var/cache/.gitkeep
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								skeleton/var/logs/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								skeleton/var/logs/.gitkeep
									
									
									
									
									
										Normal file
									
								
							@ -10,7 +10,7 @@ use Picea,
 | 
				
			|||||||
use Psr\Http\Message\ServerRequestInterface;
 | 
					use Psr\Http\Message\ServerRequestInterface;
 | 
				
			||||||
use Storage\Session;
 | 
					use Storage\Session;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use Laminas\Diactoros\Response\{ HtmlResponse, TextResponse, RedirectResponse, JsonResponse };
 | 
					use Laminas\Diactoros\Response\{ HtmlResponse, TextResponse, RedirectResponse, JsonResponse, EmptyResponse };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use Ulmus\EntityCollection;
 | 
					use Ulmus\EntityCollection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -21,12 +21,13 @@ use TheBugs\Email\MailerInterface;
 | 
				
			|||||||
use Notes\Route\Annotation\Object\Route as RouteParam,
 | 
					use Notes\Route\Annotation\Object\Route as RouteParam,
 | 
				
			||||||
    Notes\Route\Annotation\Method\Route,
 | 
					    Notes\Route\Annotation\Method\Route,
 | 
				
			||||||
    Notes\Security\Annotation\Security,
 | 
					    Notes\Security\Annotation\Security,
 | 
				
			||||||
 | 
					    Notes\Security\Annotation\Taxus,
 | 
				
			||||||
    Notes\Tell\Annotation\Language;
 | 
					    Notes\Tell\Annotation\Language;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use function file_get_contents;
 | 
					use function file_get_contents;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @Security("locked" => false)
 | 
					 * @Security("locked" => true)
 | 
				
			||||||
 * @RouteParam("methods" => [ "GET", "POST", "DELETE" ])
 | 
					 * @RouteParam("methods" => [ "GET", "POST", "DELETE" ])
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
trait ControllerTrait {
 | 
					trait ControllerTrait {
 | 
				
			||||||
@ -68,9 +69,9 @@ trait ControllerTrait {
 | 
				
			|||||||
        return new RedirectResponse($url, $code, $headers);
 | 
					        return new RedirectResponse($url, $code, $headers);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function renderPdf($rawdata, int $status = 200, array $headers = []) : PdfResponse
 | 
					    public static function renderNothing(int $code = 204, array $headers = []) : ResponseInterface
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return new PdfResponse($rawdata, $status, $headers);
 | 
					        return new EmptyResponse($code, $headers);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static function renderText(string $text, int $code = 200, array $headers = []) : ResponseInterface
 | 
					    public static function renderText(string $text, int $code = 200, array $headers = []) : ResponseInterface
 | 
				
			||||||
@ -88,6 +89,11 @@ trait ControllerTrait {
 | 
				
			|||||||
        return new JsonResponse($data, $code, $headers);
 | 
					        return new JsonResponse($data, $code, $headers);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function renderPdf($rawdata, int $status = 200, array $headers = []) : PdfResponse
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return new PdfResponse($rawdata, $status, $headers);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static function renderDownloadable(string $data, string $filename, int $code = 200, array $headers = []) : ResponseInterface
 | 
					    public static function renderDownloadable(string $data, string $filename, int $code = 200, array $headers = []) : ResponseInterface
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return new DownloadResponse($data, $filename, $code, $headers);
 | 
					        return new DownloadResponse($data, $filename, $code, $headers);
 | 
				
			||||||
 | 
				
			|||||||
@ -58,19 +58,25 @@ class Kernel {
 | 
				
			|||||||
        // Environment vars (accessible from \DI\env(), getenv(), $_ENV and $_SERVER)
 | 
					        // Environment vars (accessible from \DI\env(), getenv(), $_ENV and $_SERVER)
 | 
				
			||||||
        Dotenv::create(getenv("PROJECT_PATH"))->load();
 | 
					        Dotenv::create(getenv("PROJECT_PATH"))->load();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Paths and directories
 | 
					 | 
				
			||||||
        foreach($this->paths as $name => $envkey) {
 | 
					 | 
				
			||||||
            if ( ! getenv($name) ) {
 | 
					 | 
				
			||||||
                static::putenv($name, realpath(getenv("PROJECT_PATH") . DIRECTORY_SEPARATOR . getenv($envkey)));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Override using headers
 | 
					        // Override using headers
 | 
				
			||||||
        foreach(['APP_ENV', 'DEBUG', ] as $env) {
 | 
					        foreach(['APP_ENV', 'DEBUG', ] as $env) {
 | 
				
			||||||
            if ( null !== $value = $_SERVER["HTTP_$env"] ?? null ) {
 | 
					            if ( null !== $value = $_SERVER["HTTP_$env"] ?? null ) {
 | 
				
			||||||
                static::putenv($env, $value);
 | 
					                static::putenv($env, $value);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Paths and directories
 | 
				
			||||||
 | 
					        foreach($this->paths as $name => $envkey) {
 | 
				
			||||||
 | 
					            if ( ! getenv($name) ) {
 | 
				
			||||||
 | 
					                $path = getenv("PROJECT_PATH") . DIRECTORY_SEPARATOR . getenv($envkey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (getenv('DEBUG') && ! file_exists($path)) {
 | 
				
			||||||
 | 
					                    mkdir($path, 0755, true);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                static::putenv($name, realpath($path));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected function initializeEngine() : self
 | 
					    protected function initializeEngine() : self
 | 
				
			||||||
@ -81,6 +87,10 @@ class Kernel {
 | 
				
			|||||||
        setlocale(LC_ALL, $this->locale = getenv("DEFAULT_LOCAL"));
 | 
					        setlocale(LC_ALL, $this->locale = getenv("DEFAULT_LOCAL"));
 | 
				
			||||||
        setlocale(LC_TIME, getenv("DEFAULT_TIME"), getenv("DEFAULT_TIME_FALLBACK"));
 | 
					        setlocale(LC_TIME, getenv("DEFAULT_TIME"), getenv("DEFAULT_TIME_FALLBACK"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ( class_exists('Locale') ) {
 | 
				
			||||||
 | 
					            \Locale::setDefault($this->locale);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ini_set("log_errors", "1");
 | 
					        ini_set("log_errors", "1");
 | 
				
			||||||
        ini_set("error_log", $this->errorLogPath);
 | 
					        ini_set("error_log", $this->errorLogPath);
 | 
				
			||||||
        ini_set('display_errors', getenv("DEBUG") ? 'on' : 'on');
 | 
					        ini_set('display_errors', getenv("DEBUG") ? 'on' : 'on');
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Lean;
 | 
					namespace Lean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use function DI\autowire, DI\create;
 | 
					use Taxus\Taxus;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use League\Route\RouteGroup,
 | 
					use League\Route\RouteGroup,
 | 
				
			||||||
    League\Route\Router;
 | 
					    League\Route\Router;
 | 
				
			||||||
@ -23,6 +23,8 @@ use Picea\Picea,
 | 
				
			|||||||
use Storage\Cookie,
 | 
					use Storage\Cookie,
 | 
				
			||||||
    Storage\Session;
 | 
					    Storage\Session;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use function DI\autowire, DI\create;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Routing {
 | 
					class Routing {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected Session $session;
 | 
					    protected Session $session;
 | 
				
			||||||
@ -37,6 +39,8 @@ class Routing {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    protected LanguageHandler $language;
 | 
					    protected LanguageHandler $language;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected Taxus $taxus;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function __construct(
 | 
					    public function __construct(
 | 
				
			||||||
        Session $session,
 | 
					        Session $session,
 | 
				
			||||||
        Cookie $cookie,
 | 
					        Cookie $cookie,
 | 
				
			||||||
@ -44,7 +48,8 @@ class Routing {
 | 
				
			|||||||
        Router $router,
 | 
					        Router $router,
 | 
				
			||||||
        RouteFetcher $routeFetcher,
 | 
					        RouteFetcher $routeFetcher,
 | 
				
			||||||
        SecurityHandler $security,
 | 
					        SecurityHandler $security,
 | 
				
			||||||
        LanguageHandler $language
 | 
					        LanguageHandler $language,
 | 
				
			||||||
 | 
					        Taxus $taxus
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->session = $session;
 | 
					        $this->session = $session;
 | 
				
			||||||
@ -54,6 +59,7 @@ class Routing {
 | 
				
			|||||||
        $this->security = $security;
 | 
					        $this->security = $security;
 | 
				
			||||||
        $this->language = $language;
 | 
					        $this->language = $language;
 | 
				
			||||||
        $this->router = $router;
 | 
					        $this->router = $router;
 | 
				
			||||||
 | 
					        $this->taxus = $taxus;
 | 
				
			||||||
    } 
 | 
					    } 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function registerRoute(ContainerInterface $container, string $urlBase) {
 | 
					    public function registerRoute(ContainerInterface $container, string $urlBase) {
 | 
				
			||||||
@ -84,12 +90,16 @@ class Routing {
 | 
				
			|||||||
                        $object = $container->get($class);
 | 
					                        $object = $container->get($class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        # Checking if user needs to be logged
 | 
					                        # Checking if user needs to be logged
 | 
				
			||||||
                        if ( ! $object->user->logged && ( $redirect = $this->security->verify($class, $method) ) ) {
 | 
					                        if ( ( $redirect = $this->security->verify($class, $method) ) && ( empty($object->user) || ! $object->user->logged ) ) {
 | 
				
			||||||
                            $this->session->redirectedFrom = (string) $request->getUri();
 | 
					                            $this->session->redirectedFrom = (string) $request->getUri();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            return $redirect;
 | 
					                            return $redirect;
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if ( $forbidden = $this->security->taxus($class, $method, $object->user) ) {
 | 
				
			||||||
 | 
					                            return $forbidden;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        if ( $container->has(Picea::class) ) {
 | 
					                        if ( $container->has(Picea::class) ) {
 | 
				
			||||||
                            $container->get(Picea::class)->globalVariables['route'] = $annotation;
 | 
					                            $container->get(Picea::class)->globalVariables['route'] = $annotation;
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										26
									
								
								view/lean/error/401.phtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								view/lean/error/401.phtml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					{% extends "lean/layout/error" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% language.set "lean.error.401" %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% title _('page-title') %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% section "content-right" %}
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					        <div class="title">{% _ "title" %}</div>
 | 
				
			||||||
 | 
					        <div class="subtitle">{% _ "subtitle" %}</div>
 | 
				
			||||||
 | 
					        <div class="content">{% _ "message" %}</div>
 | 
				
			||||||
 | 
					        <u><a href="#" onclick="history.back()">{% _ "back" %}</a></u>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					{% endsection %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% section "content-left" %}
 | 
				
			||||||
 | 
					    <div class="picto-login">
 | 
				
			||||||
 | 
					        {% view "lean/picto/undraw_forbidden" %}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					{% endsection %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{% section "head.css" %}
 | 
				
			||||||
 | 
					    .title {font-size:2rem}
 | 
				
			||||||
 | 
					    .subtitle {font-size:1.25rem; padding-top: 1rem;}
 | 
				
			||||||
 | 
					    .content {padding-top:1rem}
 | 
				
			||||||
 | 
					{% endsection %}
 | 
				
			||||||
@ -14,7 +14,9 @@
 | 
				
			|||||||
{% endsection %}
 | 
					{% endsection %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% section "content-left" %}
 | 
					{% section "content-left" %}
 | 
				
			||||||
    <img class="picto-login" src="{% asset 'asset/picto/undraw_lost_bqr2.svg' %}">
 | 
					    <div class="picto-login">
 | 
				
			||||||
 | 
					        {% view "lean/picto/undraw_lost" %}
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
{% endsection %}
 | 
					{% endsection %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% section "head.css" %}
 | 
					{% section "head.css" %}
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,9 @@
 | 
				
			|||||||
{% endsection %}
 | 
					{% endsection %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% section "content-left" %}
 | 
					{% section "content-left" %}
 | 
				
			||||||
    <img class="picto-login" src="{% asset 'static/img/bugs.svg' %}">
 | 
					<div class="picto-login">
 | 
				
			||||||
 | 
					    {% view "lean/picto/undraw_error" %}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
{% endsection %}
 | 
					{% endsection %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% section "head.css" %}
 | 
					{% section "head.css" %}
 | 
				
			||||||
 | 
				
			|||||||
@ -20,7 +20,7 @@
 | 
				
			|||||||
                #wrapper-content .content-left {background:#ffffff;padding:5vh 2vw;border-radius:0 6px 6px 0;display:flex;align-items:center;justify-content: center}
 | 
					                #wrapper-content .content-left {background:#ffffff;padding:5vh 2vw;border-radius:0 6px 6px 0;display:flex;align-items:center;justify-content: center}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                .form-user-login {width:80%;}
 | 
					                .form-user-login {width:80%;}
 | 
				
			||||||
                .picto-login {max-width:80%;}
 | 
					                .picto-login svg {max-width:100%;}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                {% section "head.css" %}{% endsection %}
 | 
					                {% section "head.css" %}{% endsection %}
 | 
				
			||||||
            </style>
 | 
					            </style>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								view/lean/picto/undraw_error.phtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								view/lean/picto/undraw_error.phtml
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
		 After Width: | Height: | Size: 40 KiB  | 
							
								
								
									
										1
									
								
								view/lean/picto/undraw_forbidden.phtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								view/lean/picto/undraw_forbidden.phtml
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
		 After Width: | Height: | Size: 11 KiB  | 
							
								
								
									
										1
									
								
								view/lean/picto/undraw_lost.phtml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								view/lean/picto/undraw_lost.phtml
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
		 After Width: | Height: | Size: 6.7 KiB  | 
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user