If the application contains commercial or private information, it is wise to restrict access to the data by adding an authentication form.
My Visual Database has a built-in access control system. To activate it, go to the “Database tables” tab and click on the “Access control” button (1). In the window that opens, set the “Enable access control” checkbox (2), and then create a list of roles (4) using the edit buttons (3). For the changes to take effect, do not forget to click the “OK” button (5).
The “Settings” tab contains additional options that determine the appearance of the login entry element (1) and the ability to enter a blank password (2).
After saving the settings, two system tables will appear in the project: _role and _user.
For each user of the program, you need to create an account in the _user table and assign a role that determines access rights. In addition to the password and login, the account stores information about the user’s name, his e-mail address, the date the account was created, and the time of the last login to the program. The built-in authorization system is described in more detail in my book “Visual Programming”.
You cannot delete or edit the service fields in these tables, but you can add your own fields if necessary.
At the first launch of the program, an entry will be automatically added to the _user table so that you can log into the program as an administrator and add accounts with roles.
Now, before the appearance of the main form of the application, the authorization form is displayed:
In my opinion, the form suffers from low information content and is out of the usual style. But with the help of scripts, the situation can be corrected. Let’s take a closer look at the form using the “Component Explorer” tool, which is part of the “Developer’s Reference”.
Knowing the names of the form and components, you can modify them, making the form more understandable and functional using the App_LoginForm_OnActivate() procedure, which will be called using the onActivate event. Usually, the onShow event is used for such purposes, but in the application it is used by the system code, and if you assign your own handler to the onShow event, the authorization form will stop working.
procedure App_LoginForm_OnActivate(Sender: TObject);
// login form activation
// the OnShow event is used by the system, so it can't be overridden
var
frmpForm:TForm;
tmpImage:TImage;
tmpButton: TButton;
tmpPanLogin: TPanel;
tmpPanPassword: TPanel;
tmpEdtLogin: TEdit;
tmpEdtPassword: TEdit;
tmpCombo: TdbCombobox;
i: integer;
tmpImageFileName :string;
tmpLabel:TLabel;
tmpCheck: TdbCheckBox;
tmpLogin:string;
// for code optimization (less copy-paste)
procedure SetAttr( ACont: TControl; AParent:TWinControl; AMTop: integer; AMBottom: integer; AMLeft: integer; AMRight: integer; );
begin
with ACont do
begin
Parent := AParent;
AlignWithMargins := True;
Margins Top := AMTop;
Margins.Bottom := AMBottom;
Margins.Left := AMLeft;
Margins.Right := AMRight;
// Align := // not implemented in MVDB, but TControl does
end;
end;
begin
FindC(frmdbCoreLogin,'labLogin',tmpLabel,False);
// what is below is executed once, when the form is opened for the first time
if tmpLabel = nil then
begin
// main components can be found by name
FindC(frmdbCoreLogin,'Image1',tmpImage);
FindC(frmdbCoreLogin,'bLogin',tmpButton);
FindC(frmdbCoreLogin,'pnPassword',tmpPanPassword);
FindC(frmdbCoreLogin,'pnLogin',tmpPanLogin);
FindC(frmdbCoreLogin,'edPassword',tmpEdtPassword);
FindC(frmdbCoreLogin,'edLogin',tmpEdtLogin);
//
FindC(frmdbCoreLogin,'cmbLogin',tmpCombo,False);
if tmpCombo = nil then
begin // dropdown component can be found by type
// but it may not be there if the user input method - Text field option is selected in the access control options
for i:=0 to frmdbCoreLogin.ComponentCount - 1 do
if frmdbCoreLogin.Components[i] is TdbCombobox then
begin
tmpCombo := TdbCombobox(frmdbCoreLogin.Components[i]);
tmpCombo.Name := 'cmbLogin';
tmpCombo.Text := '';
break;
end;
end;
// read the saved login if this option was enabled
tmpLogin := IniFile_Read(APP_LOGIN_SECTION,APP_LOGIN_NAME, '' );
// the title and version are displayed in the form header
frmdbCoreLogin.Caption := APP_NAME + ' ' + App_GetVersion(False);
tmpImageFileName := ExtractFilePath(Application.ExeName)+APP_LOGIN_LOGO_FILE_NAME;
if not FileExists(tmpImageFileName) then
RaiseException('App_InitLoginForm() File not found '+tmpImageFileName)
else
tmpImage.Picture.LoadFromFile(tmpImageFileName);
// shape will be rectangular, horizontal
frmdbCoreLogin.Width := 470;
frmdbCoreLogin.Height := 250;
// there will be a panel on the right
with tmpPanPassword do
begin
Width := 220;
Align := alRight;
end;
// other panel - remaining space
with tmpPanLogin do
begin
Align := alClient;
end;
// left - picture
SetAttr( tmpImage, tmpPanLogin, 8, 8, 8, 0 );
with tmpImage do
begin
Align := alClient;
Proportional := False;
end;
// transfer everything else to the tmpPanPassword panel
// label for the login input field
tmpLabel := TLabel.Create(frmdbCoreLogin);
SetAttr( tmpLabel, tmpPanPassword, 8, 0, 8, 8 );
with tmpLabel do
begin
Name := 'labLogin';
Font.Size := 11;
Caption := 'Login:';
top := 0;
Align := alTop;
end;
//
SetAttr( tmpEdtLogin, tmpPanPassword, 2, 0, 8, 8 );
with tmpEdtLogin do
begin
Font.Size := 11;
Height := 24;
BorderStyle := bsSingle;
Top := tmpLabel.Top + tmpLabel.Height + 1;
Align := alTop;
end;
//
if tmpCombo <> nil then
begin
SetAttr( tmpCombo, tmpPanPassword, 2, 0, 8, 8 );
with tmpCombo do
begin
Font.Size := 11;
Top := tmpLabel.Top + tmpLabel.Height + 1;
Align := alTop;
end;
end;
//
tmpLabel := TLabel.Create(frmdbCoreLogin);
SetAttr( tmpLabel, tmpPanPassword, 8, 0, 8, 8 );
with tmpLabel do
begin
Name := 'labPassword';
Font.Size := 11;
Caption := 'Password:';
Top := tmpEdtLogin.Top + tmpEdtLogin.Height + 1;
Align := alTop;
end;
//
SetAttr( tmpEdtPassword, tmpPanPassword, 2, 0, 8, 8 );
with tmpEdtPassword do
begin
Font.Size := 11;
Height := 24;
BorderStyle := bsSingle;
Top := tmpLabel.Top + tmpLabel.Height + 1;
Align := alTop;
end;
//
if APP_LOGIN_SAVE_LAST_LOGIN then
begin
tmpCheck := TdbCheckBox.Create(frmdbCoreLogin);
SetAttr( tmpCheck, tmpPanPassword, 0, 8, 8, 8 );
with tmpCheck do
begin
Name := 'chbSaveLogin';
Caption := 'Save login';
Font.Size := 11;
Top := frmdbCoreLogin.ClientHeight - Height;
Align := alBottom;
Checked := tmpLogin <> '';
end;
end;
//
SetAttr( tmpButton, tmpPanPassword, 8, 8, 8, 8 );
with tmpButton do
begin
//
if tmpCheck <> nil then
Top := tmpCheck.Top - Height
else
Top := frmdbCoreLogin.ClientHeight - Height;
Align := alBottom;
end;
// if select from list mode is enabled
if tmpCombo <> nil then
begin
tmpCombo.TabOrder := 0;
tmpCombo.ItemIndex := tmpCombo.Items.IndexOf(tmpLogin);
if tmpLogin = '' then
tmpCombo.SetFocus;
end
else
begin
tmpEdtLogin.TabOrder := 0;
tmpEdtLogin.Text := tmpLogin;
if tmpLogin = '' then
tmpEdtLogin.SetFocus;
end;
//
tmpEdtPassword.TabOrder := 1;
if tmpLogin <> '' then
tmpEdtPassword.SetFocus;
tmpButton.TabOrder := 2;
if tmpCheck <> nil then
tmpCheck.TabOrder := 3;
//
frmdbCoreLogin.OnHide := @App_LoginForm_OnClose;
end;
end;
Code language: Delphi (delphi)
There is a lot of code, but the result is excellent:
- the form has acquired a horizontal format familiar to the desktop;
- the input fields have labels with their descriptions;
- the name and version of the program are displayed in the form header;
- the image is the application’s business card;
- the option to save the last selected login has been added.
Saving the last selected login reduces the access time to the program, although it slightly reduces security. But nothing more than using a drop-down list to select a login. This option can be disabled by changing the value of the global variable APP_LOGIN_SAVE_LAST_LOGIN.
Include the App_LoginForm_OnActivate() handler in the main block:
begin
// called here, before the main form is displayed and before the main application logic starts.
frmdbCoreLogin.onActivate := @App_LoginForm_OnActivate;
end.
Code language: Delphi (delphi)
Login is saved when the form is closed:
procedure App_LoginForm_OnClose(Sender: TObject; );
// close the login form
var
tmpCheck: TdbCheckBox;
tmpCombo: TdbCombobox;
tmpEdtLogin: TEdit;
tmpLogin:string;
begin
// if necessary, write parameters
FindC(frmdbCoreLogin,'edLogin',tmpEdtLogin);
FindC(frmdbCoreLogin,'cmbLogin',tmpCombo,False);
FindC(frmdbCoreLogin,'chbSaveLogin',tmpCheck,False);
// can be either a dropdown list or a login input field
if tmpCombo <> nil then
tmpLogin := tmpCombo.Text
else
tmpLogin := tmpEdtLogin.Text;
// if there is a checker and it is set, then save the login
if (tmpCheck <> nil) and tmpCheck.Checked then
begin
IniFile_Write(APP_LOGIN_SECTION,APP_LOGIN_NAME, tmpLogin );
end
else
begin
IniFile_Write(APP_LOGIN_SECTION,APP_LOGIN_NAME, '' );
end;
// reset the password
TEdit( GetC(frmdbCoreLogin,'edPassword') ).Text := '';
end;
Code language: Delphi (delphi)
Menu rights
In My Visual Database, role rights are assigned to form elements: buttons, tables, and data entry fields. But setting the main menu by role is not provided. The following procedures will help us fix this defect:
- Menu_AddAccessRight() – add permissions by role
- Menu_CheckAccessRight() – set menu visibility by role
It will also be useful to make improvements to the one described earlier in the article “Simple movements and forms” (from a series of articles “Production”) function Menu_Add() by adding one more parameter – list of roles for which this menu item will be available.
var
MenuAccessList:TStringList;
procedure Menu_AddAccessRight( AItem: TMenuItem; ARoleList:string );
// adding rights to the menu item
// AItem - menu item
// ARoleList - list of roles that are allowed access
begin
// list is created on first call
if MenuAccessList = nil then
MenuAccessList := TStringList.Create;
//
MenuAccessList.AddObject( ARoleList, AItem );
end;
procedure Menu_CheckAccessRight;
// setting the visibility of menu items according to access rights - the role of the current user
var
AItem: TMenuItem;
i: integer;
begin
if MenuAccessList <> nil then
begin
for i:=0 to MenuAccessList.Count - 1 do
begin
AItem := TMenuItem( MenuAccessList.Objects(i) );
AItem.Visible := pos( Application.User.Role, ','+MenuAccessList.Strings(i)+',' ) > 0;
end;
end;
end;
function Menu_Add( AName:string; ACaption: string; AParentItem:TMenuItem; AIndex:integer = -1; AOnClick:string = ''; ARoleList:string = ''; ):TMenuItem;
// adding a menu item
// AName - item name; action and parameter are set: <action>_<parameter> ; actions: Show - display the form on the working panel of the main form, parameter - form name
// ACaption - the displayed name of the menu item
// AParentItem - parent element; if nil, then the menu item is added to the top level
// AIndex - the place where we insert; -1 - add to the end.
// AOnClick - handler
// ARoleList - list of roles for which the menu item will be visible; if empty, then visible to everyone
var
tmpForm:TForm;
begin
tmpForm := MainForm; // work with the menu on the main form
Result := TMenuItem.Create( tmpForm );
// if the name is specified, then we format it according to the naming standard
if AName <> '' then
Result.Name := T_MENU_ITEM + AName; // add class prefix
Result.Caption := ACaption;
// if the handler is not specified, then assign a default handler
if AOnClick = '' then
AOnClick := 'Menu_ItemOnClick';
// if the handler is not disabled, then add it
if AOnClick <> '-' then
Result.OnClick := AOnClick;
// if the parent element is not specified, then
if AParentItem = nil then
begin // add a menu item to the top level
if AIndex = -1 then
tmpForm.Menu.Items.Add(Result)
else // or insert at the position specified in the parameter
tmpForm.Menu.Items.Insert(AIndex,Result);
end
else // if specified, then
begin // add as a child
if AIndex = -1 then
AParentItem.Add(Result)
else // or insert at the position specified in the parameter
AParentItem.Insert(AIndex,Result);
end;
// add rights by roles
if ARoleList <> '' then
Menu_AddAccessRight( Result, ARoleList );
end;
Code language: Delphi (delphi)
Usage example
I created three roles in the Discount app:
- Director
- Manager
- admin
The
Manager has access to all commercial information, except for the discount guide, which can only be edited by the Director. The admin role is needed to restrict access to the report designer, data export and import for Manager and Director.
Although the admin user has the Admin role, which restricts access to data, you should understand that the user who there is a mark Admin - Yes, can easily bypass this restriction by creating a new user with the desired role. Therefore, the admin role is more of a cosmetic role, hiding the main menu items that the administrator does not need.
The initialization of the main menu looks like this:
procedure UserApp_InitForm;
// form initialization
var
tmpItem:TMenuItem;
begin
try
// menu
//
tmpItem := Menu_Add('','References',nil,1, '-','Director,Manager'); // hide from admin
Menu_Add('Show_frmClient','Clients',tmpItem);
Menu_Add('Show_frmDiscount','Discounts',tmpItem,-1,'','Director'); // available only to directcor
//
frmMain.mniFile.Caption := 'Logs';
Menu_Add('Show_frmSale','Sales log',frmMain.mniFile,0,'','Director,Manager'); // hide from admin
Menu_Add('','-',frmMain.mniFile,1, '-','Director, Manager');
//
tmpItem := Menu_Add('HelpTopic','?',nil,-1, '-');
Menu_Add('Help','Help',tmpItem,0,'Help_Show');
Menu_Add('','-',tmpItem,1, '-');
Menu_Add('AboutEx','About',tmpItem,2,'App_ShowCoreAbout');
Menu_HideItem('mniAbout');
// very useful item - change user without restarting the program
Menu_Add('','Change user ',frmMain.mniOptions,1, 'App_Relogin');
// hide service items from regular users
Menu_AddAccessRight( TMenuItem( GetC(frmMain,'mniReport') ) ,'Admin');
Menu_AddAccessRight( TMenuItem( GetC(frmMain,'mniExportData') ) ,'Admin');
Menu_AddAccessRight( TMenuItem( GetC(frmMain,'mniImportData') ) ,'Admin');
//
Menu_CheckAccessRight; // set permissions by roles
except
RaiseException('UserApp_InitForm() - '+ExceptionMessage);
end;
end;
Code language: Delphi (delphi)
Pay attention to term 23, in which a menu item is created to change the user without restarting the program. The App_Relogin() handler procedure looks like this:
procedure App_Relogin(Sender:TObject);
// displaying the standard "About" form
var
tmpID: integer;
begin
tmpID := Application.User.id;
frmdbCoreLogin.ShowModal;
// User changed?
if tmpID <> Application.User.id then
begin
Menu_CheckAccessRight; // apply new rights
App_CloseAllWindow; // close all windows
end;
end;
Code language: Delphi (delphi)
If the user has changed, then we change the visibility of the main menu items and close all previously opened forms using the App_CloseAllWindow() procedure, which is written taking into account the possibility of navigator usage.
Pay attention to term 23, in which a menu item is created to change the user without restarting the program. The App_Relogin() handler procedure looks like this:
procedure App_Relogin(Sender:TObject);
// displaying the standard "About" form
var
tmpID: integer;
begin
tmpID := Application.User.id;
frmdbCoreLogin.ShowModal;
// User changed?
if tmpID <> Application.User.id then
begin
Menu_CheckAccessRight; // apply new rights
App_CloseAllWindow; // close all windows
end;
end;
Code language: Delphi (delphi)
If the user has changed, then we change the visibility of the main menu items and close all previously opened forms using the App_CloseAllWindow() procedure, which is written taking into account the possibility of navigator usage .
function GetC(AForm: TForm; AName: string;):TComponent;
// alternative function Form.FindComponent
begin
try
Result := AForm.FindComponent(AName);
except
if AForm = nil then
RaiseException('GetС() - form does not exist')
else
RaiseException('GetС('+AForm.Name+','+AName+') component not found');
end;
end;
Code language: PHP (php)
Note
Constants are required for authentication to work:
const
APP_LOGIN_LOGO_FILE_NAME = 'Images\Logo\logo_190x190.jpg';
APP_LOGIN_SAVE_LAST_LOGIN = True;
APP_LOGIN_SECTION = 'LOGIN'; // section of the initialization file
APP_LOGIN_NAME = 'login'; // parameter name
Code language: Delphi (delphi)
Please note that some procedures and functions are prefixed with the name of the module in which it is stored. For example, App_CloseAllWindow() is stored in the app.pas module and Menu_Add() is stored in the menu.pas module . The prefix is not specified for procedures and functions of a system nature that are frequently used. They are stored in the utils.pas module. For example, the GetC() function. You can read more about using modules in the article “Butterfly effect”.
Summary
The regular login form has been improved, a system of rights for the main menu has been created. For complete happiness, there is not enough information panel to display data about the current user and his role. I consider it inappropriate to place this information in the header of the main form, as this will overload the perception. I plan to create a status bar at the bottom and / or a beautiful menu on the left sidebar. This is where the user data display elements should be, preferably with a photo and other business attributes.
To be continued