From ff32eed01f783ee33caeacb0f7315612f0994f8f Mon Sep 17 00:00:00 2001 From: xue <> Date: Sun, 8 Apr 2007 21:33:23 +0000 Subject: Added Day 2 tutorial. --- demos/blog-tutorial/protected/pages/Day3/Auth.page | 102 ++++++++++ .../protected/pages/Day3/CreateAdminUser.page | 148 +++++++++++++++ .../protected/pages/Day3/CreateEditUser.page | 191 +++++++++++++++++++ .../protected/pages/Day3/CreateLoginUser.page | 158 ++++++++++++++++ .../protected/pages/Day3/CreateNewUser.page | 206 +++++++++++++++++++++ .../protected/pages/Day3/Overview.page | 26 +++ .../protected/pages/Day3/directories.gif | Bin 0 -> 10329 bytes .../blog-tutorial/protected/pages/Day3/output.gif | Bin 0 -> 10006 bytes .../blog-tutorial/protected/pages/Day3/output2.gif | Bin 0 -> 9222 bytes .../blog-tutorial/protected/pages/Day3/output3.gif | Bin 0 -> 9464 bytes 10 files changed, 831 insertions(+) create mode 100644 demos/blog-tutorial/protected/pages/Day3/Auth.page create mode 100644 demos/blog-tutorial/protected/pages/Day3/CreateAdminUser.page create mode 100644 demos/blog-tutorial/protected/pages/Day3/CreateEditUser.page create mode 100644 demos/blog-tutorial/protected/pages/Day3/CreateLoginUser.page create mode 100644 demos/blog-tutorial/protected/pages/Day3/CreateNewUser.page create mode 100644 demos/blog-tutorial/protected/pages/Day3/Overview.page create mode 100644 demos/blog-tutorial/protected/pages/Day3/directories.gif create mode 100644 demos/blog-tutorial/protected/pages/Day3/output.gif create mode 100644 demos/blog-tutorial/protected/pages/Day3/output2.gif create mode 100644 demos/blog-tutorial/protected/pages/Day3/output3.gif (limited to 'demos/blog-tutorial/protected/pages/Day3') diff --git a/demos/blog-tutorial/protected/pages/Day3/Auth.page b/demos/blog-tutorial/protected/pages/Day3/Auth.page new file mode 100644 index 00000000..82c667d7 --- /dev/null +++ b/demos/blog-tutorial/protected/pages/Day3/Auth.page @@ -0,0 +1,102 @@ + + +

Authentication and Authorization

+ +

+Before we set off to implement the user pages, we need to do some work to enable authentication and authorization. +

+ +

+We add two new modules to the application configuration as follows: +

+ + + + ...TDataSourceConfig and TActiveRecordConfig modules... + + + + + + + +

+The TAuthManager module manages the whole authentication and authorization workflow. It uses the users module as its user manager (see below). By specifying the LoginPage property, we inform the auth manager to redirect user's browser to the LoginUser page when an authorization fails. We will describe how to create LoginUser in the next subsection. +

+ +

+The user module is of class TDbUserManager which is responsible to verify the validity of a user and keep basic user data in the PHP session. The UserClass property is initialized as Application.BlogUser, which indicates the user manager would look for a BlogUser class under the directory protected (remember the alias Application refers to the protected directory) and use it to keep user's session data. +

+ +

+As we will see in later sections, in controls and pages, we can use $this->User to obtain the BlogUser object which contains the information of the user currently accessing the system. +

+ +

+Below is the implementation detail of BlogUser. Notice Active Record is used to perform DB query. For example, we use UserRecord::finder()->findByPk($username) to look for the primary key specified by $username in the users table. +

+ + +// Include TDbUserManager.php file which defines TDbUser +Prado::using('System.Security.TDbUserManager'); + +/** + * BlogUser Class. + * BlogUser represents the user data that needs to be kept in session. + * Default implementation keeps username and role information. + */ +class BlogUser extends TDbUser +{ + /** + * Creates a BlogUser object based on the specified username. + * This method is required by TDbUser. It checks the database + * to see if the specified username is there. If so, a BlogUser + * object is created and initialized. + * @param string the specified username + * @return BlogUser the user object, null if username is invalid. + */ + public function createUser($username) + { + // use UserRecord Active Record to look for the specified username + $userRecord=UserRecord::finder()->findByPk($username); + if($userRecord instanceof UserRecord) // if found + { + $user=new BlogUser($this->Manager); + $user->Name=$username; // set username + $user->Roles=($userRecord->role==1?'admin':'user'); // set role + $user->IsGuest=false; // the user is not a guest + return $user; + } + else + return null; + } + + /** + * Checks if the specified (username, password) is valid. + * This method is required by TDbUser. + * @param string username + * @param string password + * @return boolean whether the username and password are valid. + */ + public function validateUser($username,$password) + { + // use UserRecord Active Record to look for the (username, password) pair. + return UserRecord::finder()->findBy_username_AND_password($username,$password)!==null; + } + + /** + * @return boolean whether this user is an administrator. + */ + public function getIsAdmin() + { + return $this->isInRole('admin'); + } +} + + +
\ No newline at end of file diff --git a/demos/blog-tutorial/protected/pages/Day3/CreateAdminUser.page b/demos/blog-tutorial/protected/pages/Day3/CreateAdminUser.page new file mode 100644 index 00000000..36b43014 --- /dev/null +++ b/demos/blog-tutorial/protected/pages/Day3/CreateAdminUser.page @@ -0,0 +1,148 @@ + + +

Creating AdminUser Page

+ +

+The AdminUser page displays all user accounts in a list so that the administrator can perform some administrative work. For simplicity, the administrative work our blog system supports include editting a user account and deleting a user account. +

+ +

+We will display the user list in a table. Each row of the table represents a single user account, and the following columns are to be displayed: +

+ + + +

Creating Page Template

+

+We use TDataGrid to display the user accounts. Based on the above analysis, we configure the following four columns: +

+ + +

Complete page template is shown as follows:

+ + +<%@ Title="My Blog - Manage User Accounts" %> + +<com:TContent ID="Main"> + +

Manage User Accounts

+ +Create New User +
+ +<com:TDataGrid ID="UserGrid" + DataKeyField="username" + AutoGenerateColumns="false" + OnDeleteCommand="deleteButtonClicked"> + + <com:THyperLinkColumn + HeaderText="Username" + DataTextField="username" + DataNavigateUrlField="username"> + <prop:DataNavigateUrlFormatString># + $this->Service->constructUrl('users.EditUser',array('username'=>{0})) + </prop:DataNavigateUrlFormatString> + </com:THyperLinkColumn> + + <com:TBoundColumn + HeaderText="Email" + DataField="email" /> + + <com:TCheckBoxColumn + HeaderText="Administrator" + DataField="role" /> + + <com:TButtonColumn + HeaderText="Command" + Text="Delete" + ButtonType="PushButton" + CommandName="delete" /> + +</com:TDataGrid> + +</com:TContent> +
+ + +

Creating Page Class

+ +

+In the above page template, the datagrid's OnDeleteCommand event is ttached with the method deleteButtonClicked() which we shall implement in the page class. In addition, the datagrid needs to be populated with user accounts data when the page is initialized. Therefore, we write the page class as follows: +

+ + +class AdminUser extends TPage +{ + /** + * Populates the datagrid with user lists. + * This method is invoked by the framework when initializing the page + * @param mixed event parameter + */ + public function onInit($param) + { + parent::onInit($param); + // fetches all data account information + $this->UserGrid->DataSource=UserRecord::finder()->findAll(); + // binds the data to interface components + $this->UserGrid->dataBind(); + } + + /** + * Deletes a specified user record. + * This method responds to the datagrid's OnDeleteCommand event. + * @param TDataGrid the event sender + * @param TDataGridCommandEventParameter the event parameter + */ + public function deleteButtonClicked($sender,$param) + { + // obtains the datagrid item that contains the clicked delete button + $item=$param->Item; + // obtains the primary key corresponding to the datagrid item + $username=$this->UserGrid->DataKeys[$item->ItemIndex]; + // deletes the user record with the specified username primary key + UserRecord::finder()->deleteByPk($username); + } +} + + +

+In the above, the deleteButtonClicked() method is invoked whenever a "Delete" button is clicked. To determine which row of the buttons is clicked, we check the Item.ItemIndex property of the event parameter. To further identify which user account is to be deleted, we retrieve the primary key (username) value via the datagrid's DataKeys property. +

+ + +All data-bound controls have similar usage pattern. That is, set the DataSource property with the data and call dataBind() method to binds the data to the control's internal structure. + + + +

Adding Permission Check

+

+Since AdminUser should only be accessible by administrators, we need to adjust the page configuration file protected/pages/users/config.xml accordingly. +

+ + + + + + + + + + +

Testing

+

+To test the AdminUser page, visit the URL http://hostname/blog/index.php?page=users.AdminUser. You may be required to login as an administrator first if you have not done so. We shall expect to see the following result. +

+ + + +
\ No newline at end of file diff --git a/demos/blog-tutorial/protected/pages/Day3/CreateEditUser.page b/demos/blog-tutorial/protected/pages/Day3/CreateEditUser.page new file mode 100644 index 00000000..64ac6798 --- /dev/null +++ b/demos/blog-tutorial/protected/pages/Day3/CreateEditUser.page @@ -0,0 +1,191 @@ + + +

Creating EditUser Page

+ +

+The EditUser page is very similar to the NewUser. The main difference is that when EditUser is initially requested, the input fields should be initialized with existing user information. Another slight difference is that EditUser can also be accessed by normal users. +

+ +

+To determine which user account is to be editted, we use the following policy: +

+ + +

Creating Page Template

+

+As you may have guessed, the page template EditUser is largely the same as that of NewUser. Besides the difference in page title and the caption of the submit button, there are three main differences. +

+ +

+ + +<%@ Title="My Blog - Edit User" %> + +<com:TContent ID="Main"> + +

Edit User

+ +Username: +<com:TLabel ID="Username" /> + +
+Password: +
+<com:TTextBox ID="Password" TextMode="Password" /> + +
+Re-type Password: +<com:TCompareValidator + ControlToValidate="Password" + ControlToCompare="Password2" + ErrorMessage="Your password entries did not match." + Display="Dynamic" /> +
+<com:TTextBox ID="Password2" TextMode="Password" /> + +
+Email Address: +<com:TRequiredFieldValidator + ControlToValidate="Email" + ErrorMessage="Please provide your email address." + Display="Dynamic" /> +<com:TEmailAddressValidator + ControlToValidate="Email" + ErrorMessage="You entered an invalid email address." + Display="Dynamic" /> +
+<com:TTextBox ID="Email" /> + +<com:TControl Visible="<%= $this->User->IsAdmin %>"> +
+Role: +
+<com:TDropDownList ID="Role"> + <com:TListItem Text="Normal User" Value="0" /> + <com:TListItem Text="Administrator" Value="1" /> +</com:TDropDownList> +</com:TControl> + +
+First Name: +
+<com:TTextBox ID="FirstName" /> + +
+Last Name: +
+<com:TTextBox ID="LastName" /> + +
+<com:TButton Text="Save" OnClick="saveButtonClicked" /> + +</com:TContent> +
+ + +

Creating Page Class

+ +

+Based on the above description and template, we need to write a page class that initializes the inputs with the existing user information. In addition, the page class also needs to implement the saveButtonClicked() method which is attached to the "save" button's OnClick event. +

+ + +class EditUser extends TPage +{ + /** + * Initializes the inputs with existing user data. + * This method is invoked by the framework when the page is being initialized. + * @param mixed event parameter + */ + public function onInit($param) + { + parent::onInit($param); + if(!$this->IsPostBack) // if the page is initially requested + { + // Retrieves the existing user data. This is equivalent to: + // $userRecord=$this->getUserRecord(); + $userRecord=$this->UserRecord; + + // Populates the input controls with the existing user data + $this->Username->Text=$userRecord->username; + $this->Email->Text=$userRecord->email; + $this->Role->SelectedValue=$userRecord->role; + $this->FirstName->Text=$userRecord->first_name; + $this->LastName->Text=$userRecord->last_name; + } + } + + /** + * Saves the user account if all inputs are valid. + * This method responds to the OnClick event of the "save" button. + * @param mixed event sender + * @param mixed event parameter + */ + public function saveButtonClicked($sender,$param) + { + if($this->IsValid) // when all validations succeed + { + // Retrieves the existing user data. This is equivalent to: + $userRecord=$this->UserRecord; + + // Fetches the input data + $userRecord->username=$this->Username->Text; + // update password when the input is not empty + if(!empty($this->Password->Text)) + $userRecord->password=$this->Password->Text; + $userRecord->email=$this->Email->Text; + // update the role if the current user is an administrator + if($this->User->IsAdmin) + $userRecord->role=(int)$this->Role->SelectedValue; + $userRecord->first_name=$this->FirstName->Text; + $userRecord->last_name=$this->LastName->Text; + + // saves to the database via Active Record mechanism + $userRecord->save(); + + // redirects the browser to the homepage + $this->Response->redirect($this->Service->constructUrl($this->Service->DefaultPage)); + } + } + + /** + * Returns the user data to be editted. + * @return UserRecord the user data to be editted. + * @throws THttpException if the user data is not found. + */ + protected function getUserRecord() + { + // the user to be editted is the currently logged-in user + $username=$this->User->Name; + // if the 'username' GET var is not empty and the current user + // is an administrator, we use the GET var value instead. + if($this->User->IsAdmin && $this->Request['username']!==null) + $username=$this->Request['username']; + + // use Active Record to look for the specified username + $userRecord=UserRecord::finder()->findByPk($username); + if(!($userRecord instanceof UserRecord)) + throw new THttpException(500,'Username is invalid.'); + return $userRecord; + } +} + + + +The onInit() method is invoked by PRADO during one of the page lifecycles. Other commonly overriden lifecycle methods include onPreInit(), onLoad() and onPreRender(). + + +

Testing

+

+To test the EditUser page, visit the URL http://hostname/blog/index.php?page=users.EditUser&username=demo. You may be required to login first if you have not done so. Try logging in with different accounts (e.g. admin/demo, demo/demo) and see how the page displays differently. +

+ +
\ No newline at end of file diff --git a/demos/blog-tutorial/protected/pages/Day3/CreateLoginUser.page b/demos/blog-tutorial/protected/pages/Day3/CreateLoginUser.page new file mode 100644 index 00000000..61ce27b7 --- /dev/null +++ b/demos/blog-tutorial/protected/pages/Day3/CreateLoginUser.page @@ -0,0 +1,158 @@ + + +

Creating LoginUser Page

+ +

+The LoginUser page displays a login form and authenticates a user who tries to login. As described in authentication and authorization, the user's browser is automatically redirected to the LoginUser page when the user is attempting to access a privileged page, such as a user admin page. +

+ +

+The workflow of LoginUser is very similar to the Contact page: +

+
    +
  1. When a user accesses the LoginUser page, a login form is displayed;
  2. +
  3. The user fills in the username and password and clicks on the "login" button;
  4. +
  5. The LoginUser receives the "login" event and triggers the authentication sequence;
  6. +
  7. If the user enters correct username and password, the system assigns him a valid identity and redirects his browser to the desired privileged page; If not, a "password invalid" message is displayed. +
+ +

Creating Page Template

+ +

+Below we show the template for LoginPage. As we see, the page mainly contains a text box for collecting username and a text box for password. The username input is required, which is ensured by the TRequiredFieldValidator. The correctness of the password input is ensured by the TCustomValidator which invokes the page's validateUser() method when validation is performed. The page also has "login" button which invokes the page's loginButtonClicked() when it is clicked. +

+ + +<%@ Title="My Blog - Login" %> + +<com:TContent ID="Main"> + +

Login

+ +Username: +<com:TRequiredFieldValidator + ControlToValidate="Username" + ErrorMessage="Please provide your username." + Display="Dynamic" /> +
+<com:TTextBox ID="Username" /> + +
+Password: +<com:TCustomValidator + ControlToValidate="Password" + ErrorMessage="Your entered an invalid password." + Display="Dynamic" + OnServerValidate="validateUser" /> +
+<com:TTextBox ID="Password" TextMode="Password" /> + +
+<com:TButton Text="Login" OnClick="loginButtonClicked" /> + +</com:TContent> +
+ +

Creating Page Class

+ +

+Like the Contact page, the LoginUser page also needs a class file which mainly contains the implementation of event handlers attached in the page template. Here, we need to implement two methods: validateUser() and loginButtonClicked(). In validateUser(), we use the auth manager to verify if the username and password are valid. If valid, the auth manager will automatically create a user session with appropriate user identity information. +

+ + +class LoginUser extends TPage +{ + /** + * Validates whether the username and password are correct. + * This method responds to the TCustomValidator's OnServerValidate event. + * @param mixed event sender + * @param mixed event parameter + */ + public function validateUser($sender,$param) + { + $authManager=$this->Application->getModule('auth'); + if(!$authManager->login($this->Username->Text,$this->Password->Text)) + $param->IsValid=false; // tell the validator that validation fails + } + + /** + * Redirects the user's browser to appropriate URL if login succeeds. + * This method responds to the login button's OnClick event. + * @param mixed event sender + * @param mixed event parameter + */ + public function loginButtonClicked($sender,$param) + { + if($this->Page->IsValid) // all validations succeed + { + // obtain the URL of the privileged page that the user wanted to visit originally + $url=$this->Application->getModule('auth')->ReturnUrl; + if(empty($url)) // the user accesses the login page directly + $url=$this->Service->constructUrl($this->Service->DefaultPage); + $this->Response->redirect($url); + } + } +} + + + +

Testing

+ +

+So we have created the LoginUser page. We can test it by visiting the URL http://hostname/blog/index.php?page=users.LoginUser. Remember in the Creating Database subsection, we already created two user accounts (username/password): admin/demo and demo/demo. We can use them to test our login page. +

+ + + +

Adding Login/Logout Links to Master

+ +

+To provide a direct way for users to login and logout, we modify the MainLayout master control a bit. In particular, we add a "login" hyperlink which links to the LoginUser page. We also add a "logout" link button which logs out a user when it is clicked. +

+ +

+We modify the footer section of the MainLayout's template as follows. The visibility of "login" and "logout" is determined according to user's status. If the user is not logged in yet, i.e., $this->User->IsGuest is true, the "login" link is visible while the "logout" link is not; and vice versa. +

+ + + + + +

+Since the "logout" button attaches its OnClick event with a method called logoutButtonClicked(), we need to modify the class file of MainLayout as well. +

+ + +class MainLayout extends TTemplateControl +{ + /** + * Logs out a user. + * This method responds to the "logout" button's OnClick event. + * @param mixed event sender + * @param mixed event parameter + */ + public function logoutButtonClicked($sender,$param) + { + $this->Application->getModule('auth')->logout(); + $url=$this->Service->constructUrl($this->Service->DefaultPage); + $this->Response->redirect($url); + } +} + + +

+Now if we visit any page of our blog system, we should see either a link at the bottom of the page. The link displays "Login" if we have not logged in yet and "Logout" if we have logged in. If we click on "Logout", the browser will be redirected to the homepage and "Login" is displayed meaning we have logged out. +

+ +
\ No newline at end of file diff --git a/demos/blog-tutorial/protected/pages/Day3/CreateNewUser.page b/demos/blog-tutorial/protected/pages/Day3/CreateNewUser.page new file mode 100644 index 00000000..d9ef684f --- /dev/null +++ b/demos/blog-tutorial/protected/pages/Day3/CreateNewUser.page @@ -0,0 +1,206 @@ + + +

Creating NewUser Page

+ +

+The NewUser page is provided to the administrator user to create new a new user account. It needs to display a form that collects the information about the new user account. According to our database definition, we will need to collect the following information: +

+ + + +

Creating Page Template

+

+Based on the above analysis, we write the page template as follows: +

+ + +<%@ Title="My Blog - New User" %> + +<com:TContent ID="Main"> + +

Create New User

+ +Username: +<com:TRequiredFieldValidator + ControlToValidate="Username" + ErrorMessage="Please provide a username." + Display="Dynamic" /> +<com:TCustomValidator + ControlToValidate="Username" + ErrorMessage="Sorry, your username is taken by someone else. Please choose another username." + OnServerValidate="checkUsername" + Display="Dynamic" /> +
+<com:TTextBox ID="Username" /> + +
+Password: +<com:TRequiredFieldValidator + ControlToValidate="Password" + ErrorMessage="Please provide a password." + Display="Dynamic" /> +
+<com:TTextBox ID="Password" TextMode="Password" /> + +
+Re-type Password: +<com:TCompareValidator + ControlToValidate="Password" + ControlToCompare="Password2" + ErrorMessage="Your password entries did not match." + Display="Dynamic" /> +
+<com:TTextBox ID="Password2" TextMode="Password" /> + +
+Email Address: +<com:TRequiredFieldValidator + ControlToValidate="Email" + ErrorMessage="Please provide your email address." + Display="Dynamic" /> +<com:TEmailAddressValidator + ControlToValidate="Email" + ErrorMessage="You entered an invalid email address." + Display="Dynamic" /> +
+<com:TTextBox ID="Email" /> + +
+Role: +
+<com:TDropDownList ID="Role"> + <com:TListItem Text="Normal User" Value="0" /> + <com:TListItem Text="Administrator" Value="1" /> +</com:TDropDownList> + +
+First Name: +
+<com:TTextBox ID="FirstName" /> + +
+Last Name: +
+<com:TTextBox ID="LastName" /> + +
+<com:TButton Text="Create" OnClick="createButtonClicked" /> + +</com:TContent> +
+ +

+The template is not much different from the Contact template and the LoginUser page. It mainly consists of text boxes and validators. Some text boxes, such as username, are associated with two validators because of the multiple validation rules involved. +

+ + +

Creating Page Class

+ +

+From the above page template, we see that we need to write a page class that implements the two event handlers: checkUsername() (attached to the first custom validator's OnServerValidate event) and createButtonClicked() (attached to the "create" button's OnClick event). Therefore, we write the page class as follows: +

+ + +class NewUser extends TPage +{ + /** + * Checks whether the username exists in the database. + * This method responds to the OnServerValidate event of username's custom validator. + * @param mixed event sender + * @param mixed event parameter + */ + public function checkUsername($sender,$param) + { + // valid if the username is not found in the database + $param->IsValid=UserRecord::finder()->findByPk($this->Username->Text)===null; + } + + /** + * Creates a new user account if all inputs are valid. + * This method responds to the OnClick event of the "create" button. + * @param mixed event sender + * @param mixed event parameter + */ + public function createButtonClicked($sender,$param) + { + if($this->IsValid) // when all validations succeed + { + // populates a UserRecord object with user inputs + $userRecord=new UserRecord; + $userRecord->username=$this->Username->Text; + $userRecord->password=$this->Password->Text; + $userRecord->email=$this->Email->Text; + $userRecord->role=(int)$this->Role->SelectedValue; + $userRecord->first_name=$this->FirstName->Text; + $userRecord->last_name=$this->LastName->Text; + + // saves to the database via Active Record mechanism + $userRecord->save(); + + // redirects the browser to the homepage + $this->Response->redirect($this->Service->constructUrl($this->Service->DefaultPage)); + } + } +} + + +

+In the above, calling save() will insert a new row in the users table. This intuitive feature is enabled by Active Record. +

+ + +For simplicity, usernames in our blog system are case-sensitive! In many practical systems, usernames may be required to be case-sensitive. So special handling needs to be made when creating a new user account as well as performing authentication. Also, the surrounding blanks in a username may need to be trimmed when creating a new account with it. + + + +

Testing

+

+To test the NewUser page, visit the URL http://hostname/blog/index.php?page=users.NewUser. We shall see the following page output. Try enter different information into the form and see how the inputs are being validated. If all validation rules are satisfied, we shall expect the user account being created and the browser being redirected to the homepage. +

+ + + + +

Adding Permission Check

+

+During testing, you may have asked: shouldn't the NewUser page be only accessible by the administrator user? Yes, this is called authorization. We now describe how we add this permission check to the NewUser page. +

+ +

+A straightforward way of performing permission check is in the page class where we check whether $this->User->IsAdmin is true, and if not we redirect the browser to the LoginUser page. +

+ +

+PRADO offers a more systematic way of checking page access permissions. To do so, we need to use page configuration. Create a file protected/pages/users/config.xml with the content as follows: +

+ + + + + + + + + + + +

+The page configuration contains authorization rules that apply to the pages under the directory protected/pages/users. The above configuration reads that the NewUser can be accessed by users of role admin (see BlogUser.createUser() for why the word "admin"), and deny anonymous access (users="?" means guest users) for all pages under the directory. +

+ +

+Now if we visit the NewUser page as a guest, we will be redirected to the LoginUser page first. If our login is successful, we will be redirected back to the NewUser page. +

+ + +Page configuration can contain more than authorization rules. For example, it can include modules like we did in the application configuration. For a PRADO application, each page directory can have a page configuration which applies to the pages in the same directory and all its subdirectories. + + +
\ No newline at end of file diff --git a/demos/blog-tutorial/protected/pages/Day3/Overview.page b/demos/blog-tutorial/protected/pages/Day3/Overview.page new file mode 100644 index 00000000..5a83e3a4 --- /dev/null +++ b/demos/blog-tutorial/protected/pages/Day3/Overview.page @@ -0,0 +1,26 @@ + + +

User Management Overview

+ +

+In this section, we create pages that are related with user management. In particular, we implement these required features: user login/logout, creating new user account and updating/deleting user accounts. +

+ +

+According to the requirements, we need to create the following pages. To better organize our code, these user-related pages will be created under a new directory protected/pages/users. +

+ + + +

+After finishing this section, we shall expect to see the following directories and files: +

+ + + +
\ No newline at end of file diff --git a/demos/blog-tutorial/protected/pages/Day3/directories.gif b/demos/blog-tutorial/protected/pages/Day3/directories.gif new file mode 100644 index 00000000..f59fda58 Binary files /dev/null and b/demos/blog-tutorial/protected/pages/Day3/directories.gif differ diff --git a/demos/blog-tutorial/protected/pages/Day3/output.gif b/demos/blog-tutorial/protected/pages/Day3/output.gif new file mode 100644 index 00000000..0d812dd0 Binary files /dev/null and b/demos/blog-tutorial/protected/pages/Day3/output.gif differ diff --git a/demos/blog-tutorial/protected/pages/Day3/output2.gif b/demos/blog-tutorial/protected/pages/Day3/output2.gif new file mode 100644 index 00000000..749255d6 Binary files /dev/null and b/demos/blog-tutorial/protected/pages/Day3/output2.gif differ diff --git a/demos/blog-tutorial/protected/pages/Day3/output3.gif b/demos/blog-tutorial/protected/pages/Day3/output3.gif new file mode 100644 index 00000000..a11ee653 Binary files /dev/null and b/demos/blog-tutorial/protected/pages/Day3/output3.gif differ -- cgit v1.2.3