integration hydra apps
This commit is contained in:
parent
21fb28a6f0
commit
c3328a1ba0
8
.env
8
.env
|
@ -118,7 +118,7 @@ SONDE_URL=
|
||||||
# Mercure
|
# Mercure
|
||||||
MERCURE_URL=https://127.0.0.1/.well-known/mercure
|
MERCURE_URL=https://127.0.0.1/.well-known/mercure
|
||||||
MERCURE_PUBLIC_URL=https://127.0.0.1/.well-known/mercure
|
MERCURE_PUBLIC_URL=https://127.0.0.1/.well-known/mercure
|
||||||
MERCURE_JWT_SECRET="!ChangeMe!"
|
MERCURE_JWT_SECRET="!changeme!changeme!changeme!changeme!changeme!changeme!"
|
||||||
|
|
||||||
# Minio
|
# Minio
|
||||||
MINIO_URL=http://127.0.0.1:9000
|
MINIO_URL=http://127.0.0.1:9000
|
||||||
|
@ -129,6 +129,12 @@ MINIO_ROOT=
|
||||||
MINIO_PATH_STYLE=1
|
MINIO_PATH_STYLE=1
|
||||||
MINIO_SECURE=0
|
MINIO_SECURE=0
|
||||||
|
|
||||||
|
# Hydra apps
|
||||||
|
HYDRA_LOGINCHALLENGE="http://127.0.0.1:4445/oauth2/auth/requests/login?login_challenge="
|
||||||
|
HYDRA_LOGINCHALLENGEACCEPT="http://127.0.0.1:4445/oauth2/auth/requests/login/accept?login_challenge="
|
||||||
|
HYDRA_CONSENTCHALLENGE="http://127.0.0.1:4445/oauth2/auth/requests/consent?consent_challenge="
|
||||||
|
HYDRA_CONSENTCHALLENGEACCEPT="http://127.0.0.1:4445/oauth2/auth/requests/consent/accept?consent_challenge="
|
||||||
|
|
||||||
# Lock
|
# Lock
|
||||||
LOCK_DSN="postgresql://symfony:ChangeMe@127.0.0.1:5432/app?serverVersion=13&charset=utf8"
|
LOCK_DSN="postgresql://symfony:ChangeMe@127.0.0.1:5432/app?serverVersion=13&charset=utf8"
|
||||||
|
|
||||||
|
|
|
@ -185,6 +185,10 @@ app_user_minio_document:
|
||||||
controller: App\Controller\MinioController::document
|
controller: App\Controller\MinioController::document
|
||||||
|
|
||||||
#-- Access public
|
#-- Access public
|
||||||
|
app_minio_logo:
|
||||||
|
path: /minio/logo
|
||||||
|
controller: App\Controller\MinioController::logo
|
||||||
|
|
||||||
app_minio_image:
|
app_minio_image:
|
||||||
path: /minio/image
|
path: /minio/image
|
||||||
controller: App\Controller\MinioController::image
|
controller: App\Controller\MinioController::image
|
||||||
|
@ -193,6 +197,28 @@ app_minio_document:
|
||||||
path: /minio/document
|
path: /minio/document
|
||||||
controller: App\Controller\MinioController::document
|
controller: App\Controller\MinioController::document
|
||||||
|
|
||||||
|
#== Hydra =======================================================================================================
|
||||||
|
|
||||||
|
app_hydra_loginsql:
|
||||||
|
path: /hydra/loginsql
|
||||||
|
controller: App\Controller\HydraController::loginsql
|
||||||
|
|
||||||
|
app_hydra_checkloginsql:
|
||||||
|
path: /hydra/checkloginsql
|
||||||
|
controller: App\Controller\HydraController::checkloginsql
|
||||||
|
|
||||||
|
app_hydra_loginldap:
|
||||||
|
path: /hydra/loginldap
|
||||||
|
controller: App\Controller\HydraController::loginldap
|
||||||
|
|
||||||
|
app_hydra_checkloginldap:
|
||||||
|
path: /hydra/checkloginldap
|
||||||
|
controller: App\Controller\HydraController::checkloginldap
|
||||||
|
|
||||||
|
app_hydra_consent:
|
||||||
|
path: /hydra/consent
|
||||||
|
controller: App\Controller\HydraController::consent
|
||||||
|
|
||||||
#== Ckeditor ====================================================================================================
|
#== Ckeditor ====================================================================================================
|
||||||
app_ckeditor_upload:
|
app_ckeditor_upload:
|
||||||
path: /user/upload
|
path: /user/upload
|
||||||
|
|
|
@ -100,6 +100,11 @@ parameters:
|
||||||
minioPathstyle: '%env(resolve:MINIO_PATH_STYLE)%'
|
minioPathstyle: '%env(resolve:MINIO_PATH_STYLE)%'
|
||||||
minioSecure: '%env(resolve:MINIO_SECURE)%'
|
minioSecure: '%env(resolve:MINIO_SECURE)%'
|
||||||
|
|
||||||
|
hydraLoginchallenge: '%env(resolve:HYDRA_LOGINCHALLENGE)%'
|
||||||
|
hydraLoginchallengeaccept: '%env(resolve:HYDRA_LOGINCHALLENGEACCEPT)%'
|
||||||
|
hydraConsentchallenge: '%env(resolve:HYDRA_CONSENTCHALLENGE)%'
|
||||||
|
hydraConsentchallengeaccept: '%env(resolve:HYDRA_CONSENTCHALLENGEACCEPT)%'
|
||||||
|
|
||||||
sondeUse: '%env(resolve:SONDE_USE)%'
|
sondeUse: '%env(resolve:SONDE_USE)%'
|
||||||
sondeUrl: '%env(resolve:SONDE_URL)%'
|
sondeUrl: '%env(resolve:SONDE_URL)%'
|
||||||
|
|
||||||
|
|
|
@ -19,5 +19,44 @@ hydra:
|
||||||
email_verified:
|
email_verified:
|
||||||
- consent.session.id_token.email_verified
|
- consent.session.id_token.email_verified
|
||||||
|
|
||||||
|
- id: ninesql
|
||||||
|
title:
|
||||||
|
fr: NINE SQL
|
||||||
|
en: NINE SQL
|
||||||
|
description:
|
||||||
|
fr: Authentification via NINESQL
|
||||||
|
en: Authentication by NINESQL
|
||||||
|
icon_url: https://127.0.0.1:8000/minio/logo
|
||||||
|
login_url: http://127.0.0.1:8000/hydra/loginsql
|
||||||
|
consent_url: http://127.0.0.1:8000/hydra/consent
|
||||||
|
logout_url: http://127.0.0.1:8000/hydra/logoutsql
|
||||||
|
attributes_rewrite_rules:
|
||||||
|
username:
|
||||||
|
- consent.session.id_token.username
|
||||||
|
email:
|
||||||
|
- consent.session.id_token.email
|
||||||
|
firstname:
|
||||||
|
- consent.session.id_token.firstname
|
||||||
|
lastname:
|
||||||
|
- consent.session.id_token.lastname
|
||||||
|
|
||||||
|
- id: nineldap
|
||||||
|
title:
|
||||||
|
fr: NINE LDAP
|
||||||
|
en: NINE LDAP
|
||||||
|
description:
|
||||||
|
fr: Authentification via NINELDAP
|
||||||
|
en: Authentication by NINELDAP
|
||||||
|
icon_url: https://127.0.0.1:8000/minio/logo
|
||||||
|
login_url: http://127.0.0.1:8000/hydra/loginldap
|
||||||
|
consent_url: http://127.0.0.1:8000/hydra/consent
|
||||||
|
logout_url: http://127.0.0.1:8000/hydra/logoutldap
|
||||||
|
attributes_rewrite_rules:
|
||||||
|
username:
|
||||||
|
- consent.session.id_token.username
|
||||||
|
email:
|
||||||
|
- consent.session.id_token.email
|
||||||
|
firstname:
|
||||||
|
- consent.session.id_token.firstname
|
||||||
|
lastname:
|
||||||
|
- consent.session.id_token.lastname
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
body > section {
|
body > section {
|
||||||
background-color: rgba(214, 170, 214, 0.575);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="radio"]:checked ~ .app-item {
|
input[type="radio"]:checked ~ .app-item {
|
||||||
|
|
|
@ -14,6 +14,6 @@
|
||||||
"response_types": [
|
"response_types": [
|
||||||
"code"
|
"code"
|
||||||
],
|
],
|
||||||
"logo_uri": "https://upload.wikimedia.org/wikipedia/commons/e/e1/Password.svg",
|
"logo_uri": "https://127.0.0.1:8000/minio/logo",
|
||||||
"scope": "openid"
|
"scope": "openid"
|
||||||
}
|
}
|
|
@ -49,8 +49,8 @@ services:
|
||||||
- "80"
|
- "80"
|
||||||
environment:
|
environment:
|
||||||
SERVER_NAME: ':80'
|
SERVER_NAME: ':80'
|
||||||
MERCURE_PUBLISHER_JWT_KEY: '!ChangeMe!'
|
MERCURE_PUBLISHER_JWT_KEY: '!changeme!changeme!changeme!changeme!changeme!changeme!'
|
||||||
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeMe!'
|
MERCURE_SUBSCRIBER_JWT_KEY: '!changeme!changeme!changeme!changeme!changeme!changeme!'
|
||||||
MERCURE_EXTRA_DIRECTIVES: |
|
MERCURE_EXTRA_DIRECTIVES: |
|
||||||
cors_origins https://127.0.0.1:8000
|
cors_origins https://127.0.0.1:8000
|
||||||
# Comment the following line to disable the development mode
|
# Comment the following line to disable the development mode
|
||||||
|
@ -157,6 +157,7 @@ services:
|
||||||
- ./containers/hydra/clients.d:/etc/hydra/clients.d
|
- ./containers/hydra/clients.d:/etc/hydra/clients.d
|
||||||
ports:
|
ports:
|
||||||
- 7080:4444
|
- 7080:4444
|
||||||
|
- 4445:4445
|
||||||
links:
|
links:
|
||||||
- postgresql
|
- postgresql
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
|
@ -0,0 +1,197 @@
|
||||||
|
<?php
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
use App\Service\ApiService;
|
||||||
|
use App\Service\PasswordEncoder;
|
||||||
|
use App\Service\LdapService;
|
||||||
|
|
||||||
|
use App\Form\LoginType;
|
||||||
|
|
||||||
|
|
||||||
|
class HydraController extends AbstractController
|
||||||
|
{
|
||||||
|
|
||||||
|
private $apiservice;
|
||||||
|
private $passwordencoder;
|
||||||
|
private $ldapservice;
|
||||||
|
|
||||||
|
public function __construct(ApiService $apiservice,LdapService $ldapservice,PasswordEncoder $passwordencoder)
|
||||||
|
{
|
||||||
|
$this->apiservice = $apiservice;
|
||||||
|
$this->passwordencoder = $passwordencoder;
|
||||||
|
$this->ldapservice = $ldapservice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loginsql(Request $request): Response
|
||||||
|
{
|
||||||
|
|
||||||
|
$challenge = $request->query->get('login_challenge');
|
||||||
|
|
||||||
|
// S'il n'y a pas de challenge, on déclenche une bad request
|
||||||
|
if (!$challenge) {
|
||||||
|
throw new BadRequestException('pas de challenge');
|
||||||
|
}
|
||||||
|
|
||||||
|
// On vérifie que la requête d'identification provient bien de hydra
|
||||||
|
$response = $this->apiservice->run("GET",$this->getParameter('hydraLoginchallenge').$challenge,null);
|
||||||
|
if(!$response)
|
||||||
|
throw new BadRequestException('challenge invalide');
|
||||||
|
|
||||||
|
// si le challenge est validé par hydra, on le stocke en session pour l'utiliser par la suite et on redirige vers une route interne protégée qui va déclencher l'identification FranceConnect
|
||||||
|
$request->getSession()->set('hydraChallenge', $challenge);
|
||||||
|
|
||||||
|
// Création du formulaire
|
||||||
|
$form = $this->createForm(LoginType::class);
|
||||||
|
|
||||||
|
// Récupération des data du formulaire
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
|
||||||
|
// Affichage du formulaire
|
||||||
|
return $this->render("Home/loginHYDRA.html.twig", [
|
||||||
|
"useheader"=>false,
|
||||||
|
"usemenu"=>false,
|
||||||
|
"usesidebar"=>false,
|
||||||
|
"form"=>$form->createView(),
|
||||||
|
"mode"=>"SQL",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkloginsql(Request $request,ManagerRegistry $em) {
|
||||||
|
$username=$request->get('login')["username"];
|
||||||
|
$password=$request->get('login')["password"];
|
||||||
|
|
||||||
|
// user exist ?
|
||||||
|
$user=$em->getRepository("App\Entity\User")->findOneBy(["username"=>$username]);
|
||||||
|
if(!$user) return $this->redirect($this->generateUrl('app_hydra_loginsql',["login_challenge"=>$request->getSession()->get("hydraChallenge")]));
|
||||||
|
|
||||||
|
$islogin=$this->passwordencoder->verify($user->getPassword(),$password,$user->getSalt());
|
||||||
|
if(!$islogin) return $this->redirect($this->generateUrl('app_hydra_loginsql',["login_challenge"=>$request->getSession()->get("hydraChallenge")]));
|
||||||
|
|
||||||
|
$response = $this->apiservice->run("PUT",$this->getParameter('hydraLoginchallengeaccept').$request->getSession()->get('hydraChallenge'),["subject"=>$user->getEmail(),"acr"=>"string"]);
|
||||||
|
if(!$response||$response->code!="200")
|
||||||
|
throw new BadRequestException('login accept invalide');
|
||||||
|
|
||||||
|
$datas=[
|
||||||
|
"username"=>$user->getUsername(),
|
||||||
|
"email"=>$user->getEmail(),
|
||||||
|
"firstname"=>$user->getFirstname(),
|
||||||
|
"lastname"=>$user->getLastname()
|
||||||
|
];
|
||||||
|
$request->getSession()->set("datas",$datas);
|
||||||
|
|
||||||
|
$redirect=$response->body->redirect_to;
|
||||||
|
return $this->redirect($redirect, 301);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loginldap(Request $request): Response
|
||||||
|
{
|
||||||
|
|
||||||
|
$challenge = $request->query->get('login_challenge');
|
||||||
|
|
||||||
|
// S'il n'y a pas de challenge, on déclenche une bad request
|
||||||
|
if (!$challenge) {
|
||||||
|
throw new BadRequestException('pas de challenge');
|
||||||
|
}
|
||||||
|
|
||||||
|
// On vérifie que la requête d'identification provient bien de hydra
|
||||||
|
$response = $this->apiservice->run("GET",$this->getParameter('hydraLoginchallenge').$challenge,null);
|
||||||
|
if(!$response)
|
||||||
|
throw new BadRequestException('challenge invalide');
|
||||||
|
|
||||||
|
// si le challenge est validé par hydra, on le stocke en session pour l'utiliser par la suite et on redirige vers une route interne protégée qui va déclencher l'identification FranceConnect
|
||||||
|
$request->getSession()->set('hydraChallenge', $challenge);
|
||||||
|
|
||||||
|
// Création du formulaire
|
||||||
|
$form = $this->createForm(LoginType::class);
|
||||||
|
|
||||||
|
// Récupération des data du formulaire
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
|
||||||
|
// Affichage du formulaire
|
||||||
|
return $this->render("Home/loginHYDRA.html.twig", [
|
||||||
|
"useheader"=>false,
|
||||||
|
"usemenu"=>false,
|
||||||
|
"usesidebar"=>false,
|
||||||
|
"form"=>$form->createView(),
|
||||||
|
"mode"=>"LDAP",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkloginldap(Request $request,ManagerRegistry $em) {
|
||||||
|
$username=$request->get('login')["username"];
|
||||||
|
$password=$request->get('login')["password"];
|
||||||
|
|
||||||
|
// L'utilisateur se co à l'annuaire ?
|
||||||
|
$userldap=$this->ldapservice->userconnect($username,$password);
|
||||||
|
if(!$userldap)
|
||||||
|
return $this->redirect($this->generateUrl('app_hydra_loginldap',["login_challenge"=>$request->getSession()->get("hydraChallenge")]));
|
||||||
|
|
||||||
|
$userldap=$userldap[0];
|
||||||
|
|
||||||
|
// Init
|
||||||
|
$email = "$username@nomail.fr";
|
||||||
|
$lastname = $username;
|
||||||
|
$firstname = " ";
|
||||||
|
|
||||||
|
// Rechercher l'utilisateur
|
||||||
|
if(isset($userldap[$this->getParameter('ldapFirstname')]))
|
||||||
|
$firstname = $userldap[$this->getParameter('ldapFirstname')];
|
||||||
|
|
||||||
|
if(isset($userldap[$this->getParameter('ldapLastname')]))
|
||||||
|
$lastname = $userldap[$this->getParameter('ldapLastname')];
|
||||||
|
|
||||||
|
if(isset($userldap[$this->getParameter('ldapEmail')]))
|
||||||
|
$email = $userldap[$this->getParameter('ldapEmail')];
|
||||||
|
|
||||||
|
$response = $this->apiservice->run("PUT",$this->getParameter('hydraLoginchallengeaccept').$request->getSession()->get('hydraChallenge'),["subject"=>$email,"acr"=>"string"]);
|
||||||
|
if(!$response||$response->code!="200")
|
||||||
|
throw new BadRequestException('login accept invalide');
|
||||||
|
|
||||||
|
$datas=[
|
||||||
|
"username"=>$username,
|
||||||
|
"email"=>$email,
|
||||||
|
"firstname"=>$firstname,
|
||||||
|
"lastname"=>$lastname
|
||||||
|
];
|
||||||
|
$request->getSession()->set("datas",$datas);
|
||||||
|
|
||||||
|
$redirect=$response->body->redirect_to;
|
||||||
|
return $this->redirect($redirect, 301);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function consent(Request $request)
|
||||||
|
{
|
||||||
|
$challenge = $request->query->get('consent_challenge');
|
||||||
|
if (!$challenge) {
|
||||||
|
throw new BadRequestException("Le challenge n'est pas disponible");
|
||||||
|
}
|
||||||
|
|
||||||
|
// On vérifie que la requête d'identification provient bien de hydra
|
||||||
|
$response = $this->apiservice->run("GET",$this->getParameter('hydraConsentchallenge').$challenge,null);
|
||||||
|
if(!$response)
|
||||||
|
throw new BadRequestException('challenge invalide');
|
||||||
|
|
||||||
|
$response = $this->apiservice->run("PUT",$this->getParameter('hydraConsentchallengeaccept').$challenge,[
|
||||||
|
'grant_scope' => ['openid', 'offline_access'],
|
||||||
|
'session' => ['id_token' => $request->getSession()->get('datas')]
|
||||||
|
]);
|
||||||
|
|
||||||
|
if(!$response)
|
||||||
|
throw new BadRequestException('challenge not accept');
|
||||||
|
|
||||||
|
$redirect=$response->body->redirect_to;
|
||||||
|
return $this->redirect($redirect, 301);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -47,6 +47,11 @@ class MinioController extends AbstractController
|
||||||
return new Response(json_encode($output));
|
return new Response(json_encode($output));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function logo(Request $request): Response {
|
||||||
|
|
||||||
|
return $this->redirectToRoute("app_minio_image",["file"=>"uploads/logo/".$request->getSession()->get("logolight")]);
|
||||||
|
}
|
||||||
|
|
||||||
public function image(Request $request): Response
|
public function image(Request $request): Response
|
||||||
{
|
{
|
||||||
$file=$request->query->get("file");
|
$file=$request->query->get("file");
|
||||||
|
|
|
@ -365,7 +365,7 @@ class SecurityController extends AbstractController
|
||||||
$callback=($request->isSecure()?"https://":"http://").str_replace("//","/",$this->getParameter("appWeburl").$this->getParameter("appAlias").$this->generateUrl('app_home'));
|
$callback=($request->isSecure()?"https://":"http://").str_replace("//","/",$this->getParameter("appWeburl").$this->getParameter("appAlias").$this->generateUrl('app_home'));
|
||||||
$callback=substr($callback, 0, -1);
|
$callback=substr($callback, 0, -1);
|
||||||
|
|
||||||
$url.="?id_token_hint=$idtoken&scope=openid&state=$state&post_logout_redirect_uri=$callback";
|
$url.="?id_token_hint=$idtoken&scope=openid&post_logout_redirect_uri=$callback";
|
||||||
return $this->redirect($url);
|
return $this->redirect($url);
|
||||||
|
|
||||||
} else return $this->redirect($this->generateUrl("app_home"));
|
} else return $this->redirect($this->generateUrl("app_home"));
|
||||||
|
|
|
@ -75,6 +75,15 @@ class ApiService
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "PUT":
|
||||||
|
try{
|
||||||
|
$response = \Unirest\Request::put($url,$header,$query);
|
||||||
|
}
|
||||||
|
catch (\Exception $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
try{
|
try{
|
||||||
$response = \Unirest\Request::delete($url,$header,$query);
|
$response = \Unirest\Request::delete($url,$header,$query);
|
||||||
|
|
|
@ -25,7 +25,6 @@ class PasswordEncoder implements LegacyPasswordHasherInterface
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var_dump($salt);
|
|
||||||
return $this->hash($plainPassword,$salt) === $hashedPassword;
|
return $this->hash($plainPassword,$salt) === $hashedPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
{% extends "base.html.twig" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div style="text-align:center">
|
||||||
|
<img src="{{ path('app_minio_image',{file:"uploads/logo/"~app.session.get("logolight")}) }}" style="height:120px;margin-top:10px;margin-bottom:20px;">
|
||||||
|
<h1 style="border:none">{{app.session.get('appname')}}</h1>
|
||||||
|
{% if mode=="SQL" %}
|
||||||
|
{% set route="app_hydra_checkloginsql" %}
|
||||||
|
{% else %}
|
||||||
|
{% set route="app_hydra_checkloginldap" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{{ form_start(form, {'action': path(route), 'method': 'POST'}) }}
|
||||||
|
<div class="card homecard mb-3" style="width:400px; margin:auto; text-align: left;">
|
||||||
|
<div class="card-body">
|
||||||
|
{{ form_row(form.username) }}
|
||||||
|
{{ form_row(form.password) }}
|
||||||
|
{{ form_row(form.submit) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if mode == "SQL"%}
|
||||||
|
<a href="{{path("app_resetpwd01")}}" class="mt-3">Mot de passe oublié ?</a>
|
||||||
|
{% endif %}
|
||||||
|
{{ form_end(form) }}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block localscript %}
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$("#login_username").focus();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue