Fix #709: authform now store every post values
* README.rst: upgrade dependencies * mandaye/__init__.py: switch to 0.3 version * mandaye/auth/authform.py: use post values instead of password and username * mandaye/models.py: add dict management and post_values argument * mandaye/configs: upgrade configurations * mandaye/templates/ : upgrade templates for this new feature
This commit is contained in:
parent
eeec3e6b06
commit
6b1788299a
|
@ -48,14 +48,17 @@ You must install the following packages to use Mandaye
|
|||
* Beaker >= 1.6:: http://pypi.python.org/pypi/Beaker
|
||||
* Mako >= 0.4:: http://pypi.python.org/pypi/Mako
|
||||
* lxml >= 2.3:: http://pypi.python.org/pypi/lxml
|
||||
* xtraceback >= 0.3:: http://pypi.python.org/pypi/xtraceback
|
||||
* sqlalchemy-migrate:: http://pypi.python.org/pypi/sqlalchemy-migrate
|
||||
|
||||
|
||||
You can install all those dependencies quickly using pip::
|
||||
|
||||
pip install gevent poster SQLAlchemy Beaker Mako lxml gunicorn
|
||||
pip install poster SQLAlchemy Beaker Mako lxml gunicorn sqlalchemy-migrate xtraceback
|
||||
|
||||
or easy_install::
|
||||
|
||||
easy_install gevent poster SQLAlchemy Beaker Mako lxml gunicorn
|
||||
easy_install poster SQLAlchemy Beaker Mako lxml gunicorn sqlalchemy-migrate xtraceback
|
||||
|
||||
or apt-get (Debian based distributions)::
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
VERSION=0.2
|
||||
VERSION=0.3
|
||||
|
|
|
@ -3,6 +3,7 @@ Dispatcher for basic auth form authentifications
|
|||
"""
|
||||
import Cookie
|
||||
import base64
|
||||
import copy
|
||||
import re
|
||||
import traceback
|
||||
import urllib
|
||||
|
@ -14,8 +15,9 @@ from datetime import datetime
|
|||
from lxml.html import fromstring
|
||||
from urlparse import parse_qs
|
||||
|
||||
from mandaye import config
|
||||
from mandaye import config, VERSION
|
||||
from mandaye.db import sql_session
|
||||
from mandaye.exceptions import MandayeException
|
||||
from mandaye.models import Site, ExtUser, LocalUser
|
||||
from mandaye.log import logger
|
||||
from mandaye.http import HTTPResponse, HTTPHeader, HTTPRequest
|
||||
|
@ -38,78 +40,97 @@ class AuthForm(object):
|
|||
'form_url': '/myform',
|
||||
'form_attrs': { 'name': 'form40', },
|
||||
'username_field': 'user',
|
||||
'password_field': 'pwd'
|
||||
'password_field': 'pass',
|
||||
'post_fields': ['birthdate', 'card_number']
|
||||
}
|
||||
form_url, form_attrs, post_fields and username_field are obligatory
|
||||
site_name: str with the site name
|
||||
"""
|
||||
if not form_values.has_key('form_headers'):
|
||||
form_values['form_headers'] = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'Mozilla/5.0 Mandaye/0.0'
|
||||
'User-Agent': 'Mozilla/5.0 Mandaye/%s' % VERSION
|
||||
}
|
||||
|
||||
if not form_values.has_key('form_url') or \
|
||||
not form_values.has_key('form_attrs') or \
|
||||
not form_values.has_key('username_field') or \
|
||||
not form_values.has_key('password_field'):
|
||||
not form_values.has_key('post_fields') or \
|
||||
not form_values.has_key('username_field'):
|
||||
logger.critical("Bad configuration: AuthForm form_values dict must have \
|
||||
this keys: form_url, form_attrs, username_field and password_field")
|
||||
# TODO: manage Mandaye exceptions
|
||||
raise BaseException, 'AuthForm bad configuration'
|
||||
this keys: form_url, form_attrs, post_fields and username_field")
|
||||
raise MandayeException, 'AuthForm bad configuration'
|
||||
if config.encrypt_secret and not form_values.has_key('password_field'):
|
||||
logger.critical("Bad configuration: AuthForm form_values dict must have a \
|
||||
a password_field key if you want to encode a password.")
|
||||
raise MandayeException, 'AuthForm bad configuration'
|
||||
|
||||
self.form_url = form_values['form_url']
|
||||
self.form_values = form_values
|
||||
if not self.form_values.has_key('post_fields'):
|
||||
self.form_values['post_fields'] = []
|
||||
self.site_name = site_name
|
||||
|
||||
def _encrypt_pwd(self, pwd):
|
||||
def _encrypt_pwd(self, post_values):
|
||||
""" This method allows you to encrypt a password
|
||||
To use this feature you muste set encrypt_ext_password to True
|
||||
in your configuration and set a secret in encrypt_secret
|
||||
pwd: the password you want to encrypt
|
||||
return None if encryption failed
|
||||
post_values: containt the post values
|
||||
return None and modify post_values
|
||||
"""
|
||||
logger.debug("Encrypt password")
|
||||
enc_pwd = pwd
|
||||
if config.encrypt_secret:
|
||||
try:
|
||||
cipher = AES.new(config.encrypt_secret, AES.MODE_CFB)
|
||||
enc_pwd = cipher.encrypt(pwd)
|
||||
enc_pwd = base64.b64encode(enc_pwd)
|
||||
except Exception, e:
|
||||
if config.debug:
|
||||
traceback.print_exc()
|
||||
logger.warning('Password encrypting failed %s' % e)
|
||||
logger.debug("Encrypt password")
|
||||
password = post_values[self.form_values['password_field']]
|
||||
if config.encrypt_secret:
|
||||
try:
|
||||
cipher = AES.new(config.encrypt_secret, AES.MODE_CFB, "0000000000000000")
|
||||
password = cipher.encrypt(password)
|
||||
password = base64.b64encode(password)
|
||||
post_values[self.form_values['password_field']] = password
|
||||
except Exception, e:
|
||||
if config.debug:
|
||||
traceback.print_exc()
|
||||
logger.warning('Password encrypting failed %s' % e)
|
||||
else:
|
||||
logger.warning("You must set a secret to use pwd encryption")
|
||||
else:
|
||||
logger.warning("You must set a secret to use pwd encryption")
|
||||
return enc_pwd
|
||||
logger.warning("You must set a password_field to encode a password")
|
||||
|
||||
def _decrypt_pwd(self, enc_pwd):
|
||||
def _decrypt_pwd(self, post_values):
|
||||
""" This method allows you to dencrypt a password encrypt with
|
||||
_encrypt_pwd method. To use this feature you muste set
|
||||
encrypt_ext_password to True in your configuration and
|
||||
set a secret in encrypt_secret
|
||||
enc_pwd: your encoded password
|
||||
return None if encryption failed
|
||||
post_values: containt the post values
|
||||
return None and modify post_values
|
||||
"""
|
||||
logger.debug("Decrypt password")
|
||||
pwd = enc_pwd
|
||||
if config.encrypt_secret:
|
||||
logger.debug("Decrypt password")
|
||||
password = post_values[self.form_values['password_field']]
|
||||
try:
|
||||
cipher = AES.new(config.encrypt_secret, AES.MODE_CFB)
|
||||
pwd = base64.b64decode(enc_pwd)
|
||||
pwd = cipher.decrypt(pwd)
|
||||
cipher = AES.new(config.encrypt_secret, AES.MODE_CFB, "0000000000000000")
|
||||
password = base64.b64decode(password)
|
||||
password = cipher.decrypt(password)
|
||||
post_values[self.form_values['password_field']] = password
|
||||
except Exception, e:
|
||||
if config.debug:
|
||||
traceback.print_exc()
|
||||
logger.warning('Decrypting password failed: %s' % e)
|
||||
else:
|
||||
logger.warning("You must set a secret to use pwd decryption")
|
||||
return pwd
|
||||
|
||||
def replay(self, env, username, password, extra_values={}):
|
||||
def _get_password(self, post_values):
|
||||
if self.form_values.has_key('password_field'):
|
||||
if config.encrypt_ext_password:
|
||||
return self._encrypt_pwd(
|
||||
post_values[self.form_values['password_field']]
|
||||
)
|
||||
return post_values[self.form_values['password_field']]
|
||||
return None
|
||||
|
||||
def replay(self, env, post_values):
|
||||
""" replay the login / password
|
||||
env: WSGI env with beaker session and the target
|
||||
extra_values: dict with the field name (key) and the field value (value)
|
||||
post_values: dict with the field name (key) and the field value (value)
|
||||
"""
|
||||
if not "://" in self.form_url:
|
||||
self.form_url = env['target'].geturl() + '/' + self.form_url
|
||||
|
@ -155,22 +176,21 @@ this keys: form_url, form_attrs, username_field and password_field")
|
|||
params[input.name] = input.value
|
||||
else:
|
||||
params[input.name] = ''
|
||||
params[self.form_values['username_field']] = username
|
||||
params[self.form_values['password_field']] = password
|
||||
for key, value in extra_values.iteritems():
|
||||
for key, value in post_values.iteritems():
|
||||
params[key] = value
|
||||
params = urllib.urlencode(params)
|
||||
request = HTTPRequest(cookies, headers, "POST", params)
|
||||
return get_response(env, request, action, cj)
|
||||
|
||||
def _save_association(self, env, local_login, ext_username, ext_pwd, ext_birthdate=None):
|
||||
def _save_association(self, env, local_login, post_values):
|
||||
""" save an association in the database
|
||||
env: wsgi environment
|
||||
local_login: the Mandaye login
|
||||
ext_username: username of the external site (use for the replay)
|
||||
ext_pwd: password of the external site
|
||||
ext_birthdate: external birthdate (optional)
|
||||
post_values: dict with the post values
|
||||
"""
|
||||
ext_username = post_values[self.form_values['username_field']]
|
||||
if config.encrypt_ext_password:
|
||||
self._encrypt_pwd(post_values)
|
||||
site = sql_session().query(Site).\
|
||||
filter_by(name=self.site_name).first()
|
||||
if not site:
|
||||
|
@ -194,15 +214,10 @@ this keys: form_url, form_attrs, username_field and password_field")
|
|||
logger.info('New association: %s with %s on site %s' % \
|
||||
(ext_username, local_login, self.site_name))
|
||||
ext_user.login = ext_username
|
||||
if config.encrypt_ext_password:
|
||||
ext_pwd = self._encrypt_pwd(ext_pwd)
|
||||
ext_user.password = ext_pwd
|
||||
ext_user.post_values = post_values
|
||||
ext_user.local_user = local_user
|
||||
ext_user.last_connection = datetime.now()
|
||||
ext_user.site = site
|
||||
# TODO: generalize this
|
||||
if ext_birthdate:
|
||||
ext_user.birthdate = ext_birthdate
|
||||
sql_session().commit()
|
||||
env['beaker.session']['login'] = local_login
|
||||
env['beaker.session'][self.site_name] = ext_user.id
|
||||
|
@ -221,24 +236,18 @@ this keys: form_url, form_attrs, username_field and password_field")
|
|||
qs = parse_qs(env['QUERY_STRING'])
|
||||
for key, value in qs.iteritems():
|
||||
qs[key] = value[0]
|
||||
if not post.has_key('username') or not post.has_key('password'):
|
||||
logger.info('Association auth failed: form not correctly filled')
|
||||
qs['type'] = 'badlogin'
|
||||
return _302(values.get('associate_url') + "?%s" % urllib.urlencode(qs))
|
||||
username = post['username'][0]
|
||||
# TODO: generized this part (use a generic key / value table)
|
||||
extra_values = {}
|
||||
if post.has_key('birthdate') and self.form_values.has_key('birthdate_field'):
|
||||
birthdate_field = self.form_values['birthdate_field']
|
||||
birthdate = post['birthdate'][0]
|
||||
extra_values = {birthdate_field: birthdate}
|
||||
else:
|
||||
birthdate = None
|
||||
response = self.replay(env, username,
|
||||
post['password'][0], extra_values)
|
||||
post_fields = self.form_values['post_fields']
|
||||
post_values = {}
|
||||
for field in post_fields:
|
||||
if not post.has_key(field):
|
||||
logger.info('Association auth failed: form not correctly filled')
|
||||
qs['type'] = 'badlogin'
|
||||
return _302(values.get('associate_url') + "?%s" % urllib.urlencode(qs))
|
||||
post_values[field] = post[field][0]
|
||||
response = self.replay(env, post_values)
|
||||
if eval(condition):
|
||||
logger.debug("Replay works: save the association")
|
||||
self._save_association(env, login, username, post['password'][0], birthdate)
|
||||
self._save_association(env, login, post_values)
|
||||
if qs.has_key('next_url'):
|
||||
return _302(qs['next_url'], response.cookies)
|
||||
return response
|
||||
|
@ -250,19 +259,13 @@ this keys: form_url, form_attrs, username_field and password_field")
|
|||
def _login_ext_user(self, ext_user, env, condition, values):
|
||||
""" Log in an external user
|
||||
"""
|
||||
if not ext_user.login or not ext_user.password:
|
||||
if not ext_user.login:
|
||||
return _500(env['PATH_INFO'],
|
||||
'Invalid values for AuthFormDispatcher.login')
|
||||
# TODO: generized this condition
|
||||
extra_values = {}
|
||||
if ext_user.birthdate and self.form_values.has_key('birthdate_field'):
|
||||
extra_values = { self.form_values['birthdate_field']: ext_user.birthdate }
|
||||
post_values = copy.deepcopy(ext_user.post_values)
|
||||
if config.encrypt_ext_password:
|
||||
pwd = self._decrypt_pwd(ext_user.password)
|
||||
else:
|
||||
pwd = ext_user.password
|
||||
response = self.replay(env, ext_user.login,
|
||||
pwd, extra_values)
|
||||
self._decrypt_pwd(post_values)
|
||||
response = self.replay(env, post_values)
|
||||
if condition and eval(condition):
|
||||
ext_user.last_connection = datetime.now()
|
||||
sql_session().commit()
|
||||
|
|
|
@ -10,7 +10,7 @@ from mandaye.server import get_response
|
|||
|
||||
class EspaceFamilleAuth(VincennesAuth):
|
||||
|
||||
def replay(self, env, username, password, extra_values={}):
|
||||
def replay(self, env, post_values):
|
||||
""" This hack a hack method
|
||||
There is a bug in httplib so this method make a manual post
|
||||
"""
|
||||
|
@ -25,8 +25,8 @@ class EspaceFamilleAuth(VincennesAuth):
|
|||
except Exception, e:
|
||||
return _500('Espace famille: no cookie JSESSIONID', e)
|
||||
|
||||
params = {'codeFamille': username, 'motDePasse': password, 'idSession': fsession}
|
||||
body = urllib.urlencode(params)
|
||||
post_values['idSession'] = fsession
|
||||
body = urllib.urlencode(post_values)
|
||||
headers = "POST %s HTTP/1.1\r\nAccept-Encoding: identity\r\n\
|
||||
Content-Length: %d\r\nConnection: close\r\n\
|
||||
Accept: text/html; */*\r\nHost: %s\r\n\
|
||||
|
|
|
@ -5,8 +5,9 @@ from mandaye.filters import vincennes
|
|||
form_values = {
|
||||
'form_url': '/sezhame/page/connexion-abonne',
|
||||
'form_attrs': { 'id': 'dk-opac15-login-form', },
|
||||
'post_fields': ['user', 'password'],
|
||||
'username_field': 'user',
|
||||
'password_field': 'password'
|
||||
'password_field': 'password',
|
||||
}
|
||||
|
||||
auth = VincennesAuth(form_values, 'biblio', 'https://www.vincennes.fr/comptecitoyen/auth')
|
||||
|
|
|
@ -9,9 +9,9 @@ duonet_key = 'CV4j27Em0dM%3d'
|
|||
form_values = {
|
||||
'form_url': 'Connect.aspx?key=%s' % duonet_key,
|
||||
'form_attrs': { 'name': 'form1' },
|
||||
'post_fields': ['txtNomFoyer', 'txtDateNaissance', 'txtCode'],
|
||||
'username_field': 'txtNomFoyer',
|
||||
'birthdate_field': 'txtDateNaissance',
|
||||
'password_field': 'txtCode',
|
||||
'password_field': 'txtCode'
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ form_values = {
|
|||
'form_action': '%s/login.do' % folder_target,
|
||||
'form_url': '%s/index.do' % folder_target,
|
||||
'form_attrs': { 'action': 'login.do', },
|
||||
'post_fields': ['codeFamille', 'motDePasse'],
|
||||
'username_field': 'codeFamille',
|
||||
'password_field': 'motDePasse'
|
||||
}
|
||||
|
|
|
@ -6,3 +6,6 @@ class ImproperlyConfigured(Exception):
|
|||
"Mandaye is somehow improperly configured"
|
||||
pass
|
||||
|
||||
class MandayeException(Exception):
|
||||
"Mandaye generic exception"
|
||||
pass
|
||||
|
|
|
@ -1,12 +1,56 @@
|
|||
|
||||
import collections
|
||||
import json
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Column, Integer, String, DateTime
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship, backref
|
||||
from sqlalchemy.ext.mutable import Mutable
|
||||
from sqlalchemy.orm import column_property, relationship, backref
|
||||
from sqlalchemy.types import TypeDecorator, VARCHAR
|
||||
|
||||
|
||||
class JSONEncodedDict(TypeDecorator):
|
||||
"Represents an immutable structure as a json-encoded string."
|
||||
|
||||
impl = VARCHAR
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.dumps(value)
|
||||
return value
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.loads(value)
|
||||
return value
|
||||
|
||||
class MutationDict(Mutable, dict):
|
||||
|
||||
@classmethod
|
||||
def coerce(cls, key, value):
|
||||
""" Convert plain dictionaries to MutationDict. """
|
||||
if not isinstance(value, MutationDict):
|
||||
if isinstance(value, dict):
|
||||
return MutationDict(value)
|
||||
# this call will raise ValueError
|
||||
return Mutable.coerce(key, value)
|
||||
else:
|
||||
return value
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
""" Detect dictionary set events and emit change events. """
|
||||
dict.__setitem__(self, key, value)
|
||||
self.changed()
|
||||
|
||||
def __delitem__(self, key):
|
||||
""" Detect dictionary del events and emit change events. """
|
||||
dict.__delitem__(self, key)
|
||||
self.changed()
|
||||
|
||||
MutationDict.associate_with(JSONEncodedDict)
|
||||
Base = declarative_base()
|
||||
|
||||
class Site(Base):
|
||||
|
@ -22,14 +66,19 @@ class Site(Base):
|
|||
return "<Site('%s')>" % (self.name)
|
||||
|
||||
class LocalUser(Base):
|
||||
""" Mandaye local user
|
||||
""" Mandaye's user
|
||||
"""
|
||||
__tablename__ = 'local_users'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
login = Column(String(150), nullable=True, unique=True)
|
||||
login = Column(String(150), nullable=False, unique=True)
|
||||
password = Column(String(25), nullable=True)
|
||||
fullname = Column(String(150), nullable=True)
|
||||
firstname = Column(String(150), nullable=True)
|
||||
lastname = Column(String(150), nullable=True)
|
||||
fullname = column_property(firstname + " " + lastname)
|
||||
|
||||
creation_date = Column(DateTime, default=datetime.now(), nullable=False)
|
||||
last_connection = Column(DateTime, default=datetime.now())
|
||||
|
||||
def __init__(self, login=None, password=None, fullname=None):
|
||||
self.login = login
|
||||
|
@ -37,11 +86,7 @@ class LocalUser(Base):
|
|||
self.fullname = fullname
|
||||
|
||||
def __repr__(self):
|
||||
if self.login:
|
||||
return "<LocalUser('%s')>" % (self.login)
|
||||
elif self.token:
|
||||
return "<LocalUser('%s')>" % (self.token)
|
||||
return "<LocalUser>"
|
||||
return "<LocalUser('%d %s')>" % (self.id, self.fullname)
|
||||
|
||||
class ExtUser(Base):
|
||||
""" User of externals applications
|
||||
|
@ -49,9 +94,9 @@ class ExtUser(Base):
|
|||
__tablename__ = 'ext_users'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
login = Column(String(80), nullable=True)
|
||||
password = Column(String(25), nullable=True)
|
||||
birthdate = Column(String(15), nullable=True)
|
||||
login = Column(String(150), nullable=False)
|
||||
post_values = Column(JSONEncodedDict)
|
||||
creation_date = Column(DateTime, default=datetime.now(), nullable=False)
|
||||
last_connection = Column(DateTime, default=datetime.now())
|
||||
|
||||
local_user_id = Column(Integer, ForeignKey('local_users.id'), nullable=False)
|
||||
|
@ -59,16 +104,11 @@ class ExtUser(Base):
|
|||
local_user = relationship("LocalUser", backref=backref('ext_users'))
|
||||
site = relationship("Site", backref=backref('users'))
|
||||
|
||||
def __init__(self, login=None, password=None):
|
||||
def __init__(self, login=None, post_values=None):
|
||||
self.login = login
|
||||
self.password = password
|
||||
self.post_values = post_values
|
||||
|
||||
def __repr__(self):
|
||||
if self.login:
|
||||
return "<ExtUser('%s')>" % (self.login)
|
||||
elif self.token:
|
||||
return "<ExtUser('%s')>" % (self.token)
|
||||
return "<ExtUser>"
|
||||
|
||||
return "<ExtUser '%d'>" % (self.id)
|
||||
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
<form id="dk-opac15-login-form" method="post" accept-charset="UTF-8" action="${action_url}">
|
||||
<div><div class="form-item">
|
||||
<label for="edit-user">Numéro de carte : <span title="Ce champ est obligatoire." class="form-required">*</span></label>
|
||||
<input type="text" class="form-text required" value="" size="19" id="edit-user" name="username" maxlength="128">
|
||||
<input type="text" class="form-text required" value="" size="19" id="edit-user" name="user" maxlength="128">
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label for="edit-password">Mot de passe : <span title="Ce champ est obligatoire." class="form-required">*</span></label>
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
<span id="lblPassword">Nom de famille</span>
|
||||
</td>
|
||||
<td align="left">
|
||||
<input name="username" type="text" id="txtNomFoyer" autocomplete="off" style="font-weight:bold;" />
|
||||
<input name="txtNomFoyer" type="text" id="txtNomFoyer" autocomplete="off" style="font-weight:bold;" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
@ -57,7 +57,7 @@
|
|||
</td>
|
||||
|
||||
<td align="left">
|
||||
<input name="birthdate" type="text" id="txtDateNaissance" autocomplete="off" style="font-weight:bold;" />
|
||||
<input name="txtDateNaissance" type="text" id="txtDateNaissance" autocomplete="off" style="font-weight:bold;" />
|
||||
<span id="lblPassword1" style="font-size:XX-Small;font-style:italic;">(Ex: 16/06/2008)</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -67,7 +67,7 @@
|
|||
<span id="lblLogin">Code DuoNET</span>
|
||||
</td>
|
||||
<td align="left" style="padding-left:3px">
|
||||
<input name="password" type="password" maxlength="13" id="txtCode" autocomplete="off" style="font-weight:bold;width:121px;" />
|
||||
<input name="txtCode" type="password" maxlength="13" id="txtCode" autocomplete="off" style="font-weight:bold;width:121px;" />
|
||||
<span id="lblPassword2" style="font-size:XX-Small;font-style:italic;">(Ex: 1994000001001)</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -19,9 +19,9 @@
|
|||
% endif
|
||||
<div class="firstline">
|
||||
<label for="cdfmll" class="first"><b>⇒</b> Code famille</label> :
|
||||
<input type="text" id="cdfmll" class="txt" name="username" title="Indiquez votre code famille"><br>
|
||||
<input type="text" id="cdfmll" class="txt" name="codeFamille" title="Indiquez votre code famille"><br>
|
||||
<label for="mtdpss"><b>⇒</b> Mot de passe</label> :
|
||||
<input type="password" id="mtdpss" class="txt" name="password" title="Indiquez votre mot de passe"></div>
|
||||
<input type="password" id="mtdpss" class="txt" name="motDePasse" title="Indiquez votre mot de passe"></div>
|
||||
<input type="submit" class="submit" value="Associer"><br>
|
||||
</div>
|
||||
</form>
|
||||
|
|
Reference in New Issue