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

Leave a Reply

Your email address will not be published. Required fields are marked *