Let's assume for a minute that all users have access to the application to view info and add information. But editing and deleting is allowed only for certain users that are logged in. To do this let's add edit and delete functionality to the ShowDeveloperPage by adding some links on the bottom of the page:
We'll do the same for editing the developer. Let's start with the Delete operation since we haven't written that yet. Create a new method like so:
FUNCTION DeleteDeveloper()
THIS.InitSession()
Session = THIS.oSession
*** Allow only users with a password to delete this entry
IF !THIS.UserLogin()
RETURN
ENDIF
lnPK = VAL( Request.QueryString("ID") )
loDev = CREATEOBJECT("cDeveloper")
IF !loDev.Delete( lnPK )
THIS.ErrorMsg("Error deleting entry",loDev.cErrorMsg)
RETURN
ENDIF
THIS.Default()
ENDFUNC
We'll need to make one more change to the Default method to allow this to work. The template to display is referenced through Request.GetPhysicalPath(). We can't use the Physical Path in this case because the physical path for this request would be DeleteDeveloper().
Instead we have to specify the path of the page explicitly. How do we do this generically? Conveniently when we created the Process class Web Connection created a configuration object for us which contains an HTMLPagePath reference which is stored in the application INI file (WebDemo.ini). We can use this as follows:
Response.ExpandTemplate( Server.oConfig.oDevprocess.cHTMLPagePath + "default.dp" )
The oConfig object is the main Web Connection configuration object. All process classes created with the Wizard also create a private config class off this main config object. Here you can add custom properties that are application specific. Each of the properties is persisted into the INI file. This is great to store application specific configuration information that is dynamic and needs to be changed depending on location of the app. oDevProcess is the name of the config object for our sample process that we've been building and the HTMLPagePath points at our HTML directory. If we look at webDemo.ini you'll find:
[Devprocess]
Datapath=
Htmlpagepath=D:\inetpub\wwwroot\DevProcess\
which is indeed the correct path to our Web pages.
Note the call to the Login method which uses the Web Server's/Windows Basic Authentication mechanism to validate users. Make sure you enable Basic Authentication on the Web Server or the virutal directory that you're working with - Web Connection does this automatically for the virtual when it created the virtual directory.
Now when we run the form we'll see:
The ANY parameter on the Login() method specifies that any logged in user in the system will be allowed access so as long as you put in a valid username or password you'll get in.
You can also specify a specific username or a comma delimited list of usernames that are allowed access. If you fail to login - no access, plain and simple.
If I wanted to only allow myself I'd specify:
IF !Login("ricks")
RETURN
ENDIF
Basic Authentication is very simple to use and allows you to control access to an application neatly.
Many times however you'll find you'll want to use FoxPro tables to validate access. To do this we'll have to do a little more work. Assume you have a user table called users with two fields username and password in them. You can then use the following code:
FUNCTION UserLogin
*** Session must be available for this to work
lcUser = Session.GetSessionVar("LogonUser")
IF !EMPTY(lcUser)
RETURN .T.
ENDIF
lcuser = Request.Form("txtUserName")
lcPass = Request.Form("txtPassword")
IF !USED("Users")
USE USERS IN 0
ENDIF
SELECT Users
IF !EMPTY(lcUser)
LOCATE FOR LOWER(username) = LOWER(PADR(lcUser,20)) AND ;
LOWER(password) = LOWER(PADR(lcPass,15))
IF FOUND()
Session.SetSessionVar("LogonUser",lcUser)
RETURN .T.
ENDIF
ENDIF
*** User Input form
Response.HTMLHeader("Customer Login")
Response.Write([<form action="" method="POST">])
Response.Write([<table align=Center border=0 cellpadding=0 cellspacing=0 style="background:LightGrey;font:normal normal 8pt Tahoma">] + ;
[<tr style="background:Navy;color:White;font:normal bold 8pt Tahoma"><td colspan=2>User Login</td>] + ;
[<tr><td>Username:</td>] + ;
[<td><input type="text" size=20 name="txtUserName"></td></tr>] +;
[<tr><td>Password:</td><td><input type="password" size=15 name="txtPassword"></td></tr>] +;
[<tr><td><td><input type="submit" value="Log in"</td></tr>] + ;
[</table></form>])
RETURN .F.
ENDFUNC
* WebProcess :: UserLogin
This request will present an HTML based login dialog that validates against a Fox table and it works pretty much the same way as the Login() method in wwProcess work by default. Note that in order for this to work you a Session that is active. There's actually a little quirk relating to Sessions that occurred when I wrote this code. We need to add InitSession to the DeleteDeveloper call so that the session is available. But since we're redirecting to Default() at the end we run into trouble because Default also initializes a Session object. To avoid this problem I used a different approach using an indirect redirect (using an HTML <META REFRESH> tag):
FUNCTION DeleteDeveloper()
THIS.InitSession()
Session = THIS.oSession
*** Allow only users with a password to delete this entry
IF !THIS.UserLogin()
RETURN
ENDIF
lnPK = VAL( Request.QueryString("ID") )
loDev = CREATEOBJECT("cDeveloper")
IF !loDev.Delete( lnPK )
THIS.ErrorMsg("Error deleting entry",loDev.cErrorMsg)
RETURN
ENDIF
*** Just re-run the list now
THIS.StandardPage(loDev.oData.Company + " deleted",,,2,"default.dp")
Not the call toStandardPage() which includes a 4th and 5th parameter which specify when to redirect and to what link. This works better than a Response.Redirect() because you can write out HTTP headers (like the Cookie for the session for example) using this approach where Response.Redirect() does not support headers. Now the page displays an intermediate HTML page which in turn loads the default page.
There's actually an easier and more logical way to handle this Session problem and that is to simply load the Session in the Process() method and make it available to all methods thus avoiding any duplication of InitSession. If you do use InitSession selective just be careful that code relying on sessions doesn't call InitSession twice or you'll blow away the original session content.
Ok so we have security that's based on access rights. For editing developers we want to have similar functionality for 'admin' type users, but we also want the actual developer to edit their own entries. We won't implement this here, but basically what you want to do is assign the developer an ID in an external table that maps him to a site id. I do this on the West Wind site by using a master customer table that users are bound to when they come to the site. Users can reattach to their profile and they need that profile ID in order to access it. The way this works is that there's a user specific cookie, which maps to a user Id in my customer table. The customer PK is then a foreign key in the developer table. I deliberately left out this part of the application because the management of the customer in this scenario is fairly involved and not really very educational in terms of using business objects or providing a glimpse at any special technology. So I leave this particular excercise for you to complete and bind user profiles. It's not difficult - it just takes a fair amount of application specific code.