Pushing Podio

Fun Experiments pushing Citrix Podio to the limit so you can get more done.
... with a little help from GlobiFlow, ProcFu, and other friends ...

Building a Podio User Portal with ProcFu (Advanced)

- Posted in Uncategorized by

A common business requirement is to allow external customers to interact with Podio data.

There be some advanced code ahead. For a really simple way to build a portal on your website, see this post instead.

In this example, we're going to allow customers to:

  • log in
  • edit their profile
  • view their projects and add comments and files to active projects
  • create new projects

The demo is available here.

Note that to make the demo easier to use for demonstration purposes, we're allowing you to create your own account, which basically is a webform and a GlobiFlow flow that generates a password and sends it to you. In your production system you may wish to only manually provision new users.

This portal will also be fully embedded in your website. It's all generated via a ProcFu HTML Widget. Although we could iframe the widget, I like to rather use jQuery to get the widget and then insert it the current page. It just looks cleaner to the end user.

So, customers will be able to log in:

Once logged in, they will see a list of current projects on a simplified dashboard:

From here, they can edit their profile:

Add a new project:

And view and comment on existing projects:

Go ahead - try the DEMO

Part 1 - the Customer

The first thing we need is a customers app in Podio:

The important parts here are the Email Address and the MD5 password. We don't want to be storing a customer's real password, so we'll only store the MD5 hash of it. And don't worry - the md5() function is available in GlobiFlow for you to automate this if needed.

In our widget, we need to decode the payload:

// pre work
$data = @json_decode($pf_payload, true);

Since URL parameters are lost in GET in a POST (which we do in the login form), we need to check for the session id in the POST part:

// if we're returning from our login, pfsessid is in POST and not GET
if ( isset($data['POST']['pfsessid']) ) $data['GET']['pfsessid'] = $data['POST']['pfsessid'];

And ensure we have a valid session ID:

// make sure we have a valid session ID
if ( ! isset($data['GET']) || ! isset($data['GET']['pfsessid']) ) return "Bad Session ID";
$pfsessid = $data['GET']['pfsessid'];
pf_session_start($pfsessid);
$session = pf_session_get();
$localUrl = "https://procfu.com/widgets/html/".$data['widget_id']."&pfsessid=".$pfsessid;

I'm leaving out some stuff for clarity here, like configuration and basic HTML and CSS production. Here is the full source for the widget for reference.

Once we have our session running, let's add the required PF javascript:

// add PF javascript
$html .= pf_ui_include_js();

Now, this is the only tricky bit. Because we want to protect user credentials, we POST them instead of GETting them. But we can't POST to our website, so we POST to the widget directly, which will save some stuff to the session and then send the user back to the main page on your website:

// check if we're returning from login
if ( isset($data['POST']) && isset($data['POST']['user']) ) {
    $user = $data['POST']['user'];
    $pass = @$data['POST']['pass'];
    $session['tempuser'] = $user;
    $session['temppass'] = $pass;
    pf_session_set($session);
    return '<script>document.location.href="'.$publicPage.'";</script>';
}

If we're returning from the login intermediate step, we need to validate the user, and go to the dashboard if successful or error out otherwise:

// return from login pt 2
if ( isset($session['tempuser']) ) {
    $user = trim(strtolower($session['tempuser']));
    $pass = trim(strtolower($session['temppass']));
    unset($session['tempuser']);
    unset($session['temppass']);
    pf_session_set($session);
    $foo = @json_decode(call_pf_script("podio_search_app.pf", ["app_id" => $user_app_id, "field_id" => $user_app_email_field, "search_val" => $user, "condition" => "E", "max_results" => 1]), true);
    if ( ! is_array($foo) || sizeof($foo) == 0 ) return "<h3>Error</h3><p>Invalid Credentials (1)</p>";
    $bar = pf_podio_item_as_field_array($foo[0]);
    if ( $bar[$user_app_email_field] != $user) return "<h3>Error</h3><p>Invalid Credentials (2)</p>";
    if ( $bar[$user_app_pass_field] != md5($pass) ) return "<h3>Error</h3><p>Invalid Credentials (3)</p>";
    // login was successful
    $userEmail = $user;
    $userRecordId = $foo[0]['item_id'];
    $session['userRecordId'] = $userRecordId;
    $session['userEmail'] = $userEmail;
    pf_session_set($session);
}

If none of the above conditions are met, we now continue with our authentication logic and check if the user is logged in or not. If not, we show a log in form:

// check if we're logged in
if ( ! isset($session['userEmail']) ) {
    $html .= '<h3>Login Required</h3>';
    $html .= '<form class="pf-form" method="POST" action="'.$localUrl.'">';
    $html .= '<input type="hidden" name="pfsessid" value="'.$pfsessid.'">';
    $html .= '<div class="pf-form-row">';
    $html .= '<label class="pf-label" for="user">Email Address</label>';
    $html .= '<input class="pf-input" type="text" name="user" id="user">';
    $html .= '</div>';
    $html .= '<div class="pf-form-row">';
    $html .= '<label class="pf-label" for="user">Password</label>';
    $html .= '<input class="pf-input" type="password" name="pass" id="pass">';
    $html .= '</div>';
    $html .= '<div class="form-row">';
    $html .= '<label class="pf-label" for="submit"></label>';
    $html .= '<input class="pf-input" type="submit" name="submit" id="submit" value="Log In">';
    $html .= '</div>';
    $html .= '</form>';
    $html .= '<p style="margin-top: 25px; text-align: center"><a href="'.$publicPage.'?create">Create Account</a> or <a href="'.$publicPage.'?create">Reset Password</a></p>';
    return $html;
} 

By this point, after all the code above, we know for sure a valid user is logged in

// OK - we're logged in
$userEmail = $session['userEmail'];
$userRecordId = $session['userRecordId'];

Later, when we provide the dashboard HTML, there will be an option for the customer to edit their profile. This will be passed as a URL parameter, so we just check for it and show the edit screen:

// page: edit profile
if ( isset($data['GET']['editprofile']) ) {
    if ( ! isset($session['userapp']) ) {
        $ret = @json_decode(call_pf_script("podio_app_get_raw.pf", ["app_id" => $user_app_id]),true);
        if ( $ret === null ) return "<h3>Error</h3><p>An unexpected error has occured (4)</p>";
        $session['userapp'] = $ret;
        pf_session_set($session);
    }
    $userRecord = @json_decode(call_pf_script("podio_item_get_raw.pf", ["podio_item_id" => $session['userRecordId']]), true);
    if ( $userRecord === null ) return "<h3>Error</h3><p>Could not retrieve user record.</p>";
    $html .= '<h3>Edit Profile</h3>';
    $ui = new pf_ui_item($session['userapp'], $userRecord);
    $ui->setMode("edit");
    $ui->setSuccessUrl($publicPage);
    $ui->setFields(["title", "name", "about", "podio-user-type"]);
    $ui->setReadOnly(["title"]);
    $html .= $ui->render();
    return $html;
}

That covers most of the customer-specific functions. Let's move on to Projects...

Part 2 - Projects

As we stated in the beginning, we want customers to be able to view and comment on existing projects, as well as create new ones.

For this we need a Projects App in Podio:

Most of the fields are totally up to you. The only important field here is the relationship field back to our customers app (called "Customer" in this case).

After authentication, we need to get all of the current projects for the logged in customer:

// do we have a project list?
if ( ! isset($session['projects']) ) {
    $ret = @json_decode(call_pf_script("item_get_referenced.pf", ["podio_item_id" => $userRecordId]), true);
    if ( $ret === null ) return "<h3>Error</h3><p>An unexpected error has occured (4)</p>";
    $session['projects'] = [];
    foreach ( $ret as $ref ) {
        if ( $ref['app']['app_id'] == $project_app_id ) {
            foreach ( $ref['items'] as $item ) {
                $session['projects'][] = ["item_id"=>$item['item_id'], "title"=>$item['title']];
            }
        }
    }
    pf_session_set($session);
}

And for speed, we store this in the session as well.

All the project functions will again be sent via URL parameters. First let's check if the user clicked on the new project link and show them a form to create one:

// page: NEW project
if ( isset($data['GET']['newproject']) ) {
    if ( ! isset($session['projapp']) ) {
        $ret = @json_decode(call_pf_script("podio_app_get_raw.pf", ["app_id" => $project_app_id]),true);
        if ( $ret === null ) return "<h3>Error</h3><p>An unexpected error has occured (5)</p>";
        $session['projapp'] = $ret;
        pf_session_set($session);
    }
    $html .= '<h3>Create New Project</h3>';
    $ui = new pf_ui_item($session['projapp'], null);
    $ui->setMode("create");
    $ui->setSuccessUrl($publicPage);
    $ui->setFields(["title", "status", "details", "customer"]);
    $ui->setValue("customer", $userRecordId);
    $ui->setValue("status", "New");
    $ui->setHidden(["customer", "status"]);
    $ui->setAllowFiles(true);
    $html .= $ui->render();
    unset($session['projects']);
    pf_session_set($session);
    return $html;
}

If they clicked on an existing project, we first make sure the ID is valid (don't want users looking at other people's projects by manipulating the URL), and then show the project and allow comments to be added:

// page: view project
if ( isset($data['GET']['project']) ) {
    $projid = intval($data['GET']['project']);
    // make sure we're allowed to view this
    $projlist = array_column($session['projects'], "item_id");
    if ( ! in_array($projid, $projlist) ) return "<h3>Error</h3><p>You are not authorized to view that project.</p>";
    if ( ! isset($session['projapp']) ) {
        $ret = @json_decode(call_pf_script("podio_app_get_raw.pf", ["app_id" => $project_app_id]),true);
        if ( $ret === null ) return "<h3>Error</h3><p>An unexpected error has occured (5)</p>";
        $session['projapp'] = $ret;
        pf_session_set($session);
    }
    $project = @json_decode(call_pf_script("podio_item_get_raw.pf", ["podio_item_id" => $projid]), true);
    if ( $project === null ) return "<h3>Error</h3><p>Could not retrieve project.</p>";
    $html .= '<h3>Project: '.$project['title'].'</h3>';
    $ui = new pf_ui_item($session['projapp'], $project);
    $ui->setMode("view");
    $ui->setSuccessUrl($publicPage);
    $ui->setFields(["title", "status", "details"]);
    $ui->setShowFiles(true);
    $html .= $ui->render();
    $html .= '<form class="pf-form"><div class="pf-form-row"><div class="pf-label"><b>Comments</b></div><div class="pf-value">';
    $ui->setAppNameReplace("You (".$userEmail.")");
    $html .= $ui->getComments();
    $html .= '</div></div></form>';
    $ui = new pf_ui_comment($session['projapp'], $project);
    $ui->setAllowFiles(true);
    $html .= $ui->render();
    return $html;
}

And at the very end, if none of the above conditions have been met, our default state will be to show the user their "dashboard" of current projects and options:

$html .= '<table class="maintab"><tr><td>';
$html .= '<h5>My Projects</h5>';
if ( sizeof($session['projects']) == 0 ) {
    $html .= '<p>You do not have any current projects</p>';
} else {
    foreach ( $session['projects'] as $project ) {
        $html .= '<p><a class="projlink" href="?project='.$project['item_id'].'">';
        $html .= $project['title'].'</a></p>';
    }
}
$html .= '<p><a href="?newproject">+ New Project</a></p>';

$html .= '</td><td class="profile">';

$html .= 'Logged in as:<br>'.$userEmail.'<br>';
$html .= '<a href="?editprofile">Edit Profile</a><br>';
$html .= '<a href="?logout">Log Out</a>';

$html .= '</td></tr></table>';

return $html;

That's it for ProcFu. Now we just need to add this to our website.

Part 3 - the Website

To add this widget to our website, we'll again use jQuery to fetch the HTML it generates and put it in a div instead of iframing the widget. Just looks cleaner.

First, make sure that jQuery is included in your site:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

We don't care much about the version, just make sure it's there.

Next, we need a div on our website for the widget HTML to go into:

    <div id="pfcontent">
        <img src="/img/progress-bar.gif">
    </div>

Lastly, we need some javascript to (1) generate a new session if none exists, (2) fetch the HTML from the widget, and (3) display the HTML:

<script>
var pfwidgeturl = "https://procfu.com/widgets/html/123redacted456";
$(function() {
    if ( ! sessionStorage ) {
        $("#pfcontent").html('<h1>Error</h1><p>Your browser does not support session storage. Please use a modern browser</p>');
        return false;
    }
    var pfsessid = sessionStorage.getItem("pfsessid");
    if ( pfsessid == undefined ) {
        pfsessid = (new Date().getTime()).toString(16) + Math.random().toString(16).substring(2, 15);
        sessionStorage.setItem("pfsessid", pfsessid);
    }
    var params = window.location.search;
    params = new URLSearchParams(window.location.search);
    if ( ! params.has("pfsessid") ) params.append("pfsessid", pfsessid);
    $("#pfcontent").load(pfwidgeturl+"?"+params, function(){
    });
});
</script>

And that's it. You now have a portal for customers to log in and interact with LIVE Podio data.

Go ahead - try the DEMO

Here is the full source for the widget.

Happy hacking :-)

Comments