00:00
00:00
Newgrounds Background Image Theme

MagDeWarrior just joined the crew!

We need you on the team, too.

Support Newgrounds and get tons of perks for just $2.99!

Create a Free Account and then..

Become a Supporter!

Project got declined on Codecanyon

1,153 Views | 5 Replies
New Topic Respond to this Topic

I spent 2 months working on a user management library, with the intention of selling it on Codecanyon (I've never sold anything there before).

On Codecanyon, there's a screening process for everything that's uploaded, and five days later, aka today, I got an email from them saying this:

Unfortunately your submission doesn't meet our current minimum technical quality requirements for the category it was submitted to. Please consider taking some time to familiarize yourself with our current library and quality levels of popular items before resubmitting.

Here's the demo: http://nauthentication.recgr.com/

Here's a source code excerpt from the main class (user registration method):

/** 
 * Account registration method.
 *
 * Validates the input received and inserts it into the database. By default, this method will also send the activation email, and require the user to activate (verify the email address) its account before being able to log in. If $Account_Activation_Required is set to *false* in the Config file, then this method will not send the activation email, but it will instead activate the account automatically, and, will instead of the *activation token*, return *user id*.
 * 
 * *Example:*
 * ```php
 * // assumes $_POST is coming from a registration form, and can look something like this:
 * $_POST = array(
 *     'email' => 'jane.doe@gmail.com'
 *     'password' => 'my_password_456'
 *     // optionally, more fields
 * );
 *
 * \NAuthentication\Auth::register($_POST);
 * ```
 *
 * @param string[] $userData Supplied user credentials to insert into the database records. This will usually be email address and password. Passed as an array, where field names are array indexes, and field values are array values.
 *
 * @throws NAuthException
 *
 * @return string|int By default, returns user's activation token which was sent to the supplied email address. The token is to be used in activate() method. If $Account_Activation_Required is set to *false* in the Config file, user ID (UID) will be returned instead.
 */
public static function register(array $userData) {
    // If either, but not both email or password fields are enforced, that's an NAuthException (password requires email to verify)
    if (in_array('email', Config::$Mandatory_Params) xor in_array('password', Config::$Mandatory_Params)) {
        throw new NAuthException(Config::$Auth_Exception_Messages['emailPasswordDependency'] . __METHOD__, 14);
    }

    $expectedVsSupplied = array_diff_key(array_flip(Config::$Mandatory_Params), $userData);

    if (count($expectedVsSupplied) > 0) {
        throw new NAuthException(Config::$Auth_Exception_Messages['mandatoryParamsMissing'] . implode(", ", array_flip($expectedVsSupplied)), 11);
    } 

    // Mandatory params mustn't be empty
    foreach (Config::$Mandatory_Params as $mandatoryParam) {
        if (empty($userData[$mandatoryParam])) {
            throw new NAuthException(Config::$Auth_Exception_Messages['mandatoryParamEmpty'] . $mandatoryParam . ' in ' . __METHOD__, 12);
        }
    }

    if (isset($userData['email'])) {
        if (!filter_var($userData['email'], FILTER_VALIDATE_EMAIL)) throw new NAuthException(Config::$Auth_Exception_Messages['invalidEmailFormat'], 1);

        if (self::userExists(['email' => $userData['email'], 'activated' => 'Y'])) {
            throw new NAuthException(Config::$Auth_Exception_Messages['userEmailAlreadyExists'], 2);
        }
    }

    // check if there are any input fields that don't have a coresponding database field, and throw an NAuthException if there are (there's an identical check in authenticate(), but that simply discards any non-existing fields)
    $usersTableFields = Utils::getTableFields(Config::$Users_Tablename);

    $neededFieldsVsExistingFields = array_diff_key($userData, array_flip($usersTableFields));

    if (count($neededFieldsVsExistingFields) > 0) {
        $usedFieldsNotInDatabase = implode(', ', array_flip($neededFieldsVsExistingFields));

        throw new NAuthException(
            Config::$Auth_Exception_Messages['missingDatabaseFields'] . 'Used fields which are not in database: ' . $usedFieldsNotInDatabase . ' in table ' . Config::$Users_Tablename . ', at ' . __METHOD__, 4
        );
    }

    if (isset($userData['password'])) {
        if (strlen($userData['password']) < Config::$Minimum_Password_Length) throw new NAuthException(Config::$Auth_Exception_Messages['passwordTooShort'] . Config::$Minimum_Password_Length, 5);

        $userData['password'] = password_hash($userData['password'], PASSWORD_BCRYPT);
    }

    $link = Utils::getDatabaseInstance();

    if (self::isIPWithinRetriesLimit('register', (isset($userData['email']) ? $userData['email'] : NULL)) === false) {
        // in case user already tried to sign up, but the activation email never came, and the user didn't use the
        // resendActivationEmail feature, resend it for the user
        $activationToken = self::getTokenFromLogs('register', (isset($userData['email']) ? $userData['email'] : NULL));

        if (is_null($activationToken)) {
            // this should never execute, except if someone edited tablesCleanUp() intervals on their own and and messed it up (rows in the logs table should always have longer lifetime than the rows in activations table)
            throw new NAuthException(Config::$Auth_Exception_Messages['registerLimitExceed'], 27);
        }

        return self::resendActivationEmail($activationToken);
    }

    // PART ONE: Insert User into DB
    $sql = Utils::generatePdoInsertSql($userData, Config::$Users_Tablename);
    $stmt = $link->prepare($sql);
    $stmt = Utils::bindPdoParams($stmt, $userData);

    if (!$stmt->execute()) {
        throw new NAuthException(Config::$Auth_Exception_Messages['registrationQueryFailed'], 6);
    }

    $uid = $link->lastInsertId();
    $stmt = null;

    // PART TWO: Generate activation token and insert it into DB
    $activationToken = self::insertTokenToDatabase($uid, 'activation', $link);

    // PART THREE: Compose and send activation email
    if (Config::$Account_Activation_Required) {
        if (isset($userData['email'])) {
            $activationMessage = self::composeEmailMessage('activation', $activationToken);
            $sendEmail = self::sendEmail($userData['email'], Config::$Account_Activation_Subject, $activationMessage);
        }

        self::addIPToLog('register', (isset($userData['email']) ? $userData['email'] : NULL), $activationToken);
    } else {
        return self::activate($activationToken, $link);
    }

    // GC
    if (1/100 === 50) {
        self::tablesCleanUp($link);
    }

    return $activationToken;
}

Do you guys also think it's bad?

Improvement suggestions I've gotten so far, and that I yet have to do, are regarding line length of my lines - I have to shrink them so they're about 80 chars long.

Another question that's not directly related to code review: what do you guys think my next step should be? Should I sell it on a bunch of other platforms buying/selling platforms, or release it on Github and try to make money by people hiring me to customize/integrate it for them? My idea was to set a low price for the library itself ($5), but try to make some real profit with people hiring me to customize/implement it for them.

-Nino

Response to Project got declined on Codecanyon 2016-04-08 11:53:23


ok, this forum is dead

Response to Project got declined on Codecanyon 2016-04-08 18:22:05


At 4/8/16 11:53 AM, NinoGrounds wrote: ok, this forum is dead

Yeah, it doesn't get much traffic nowadays. And I can't really help because I've never heard of Codecanyon before and I'm not familiar with the API you're using.

But, from a quick skim, it doesn't look like what you wrote is MVC-compliant, which is something I would improve upon.

I also have no idea what the point of this is or why it's even in your source code:

if (1/100 === 50) {
    self::tablesCleanUp($link);
}

Response to Project got declined on Codecanyon 2016-04-09 09:23:23


At 4/8/16 06:22 PM, Diki wrote:
At 4/8/16 11:53 AM, NinoGrounds wrote: ok, this forum is dead
Yeah, it doesn't get much traffic nowadays. And I can't really help because I've never heard of Codecanyon before and I'm not familiar with the API you're using.

Codecanyon is a marketplace for software. I am not using any API.


But, from a quick skim, it doesn't look like what you wrote is MVC-compliant, which is something I would improve upon.

I wasn't aiming it at being MVP compliant, but there's a strict separation of code and presentation. The classes handle the logic (an excerpt which I posted), and the examples pages handle the presentation. There are also docs pages and installation instructions and library overview.


I also have no idea what the point of this is or why it's even in your source code:

if (1/100 === 50) {
self::tablesCleanUp($link);
}

It's a garbage collection bit, and PHP uses the same thing internally for its garbage collection. Basically, what it does is, every 100 times (on average), it's gonna trigger the database GC method.

/**
 * Makes sure library-related tables are kept fit.
 *
 * This method is called internally by register() method. In other words, you don't need to call it manually.
 *
 * @return bool
*/
public static function tablesCleanUp() {
    $link = Utils::getDatabaseInstance();
    
    // expire activation/password reset records older than a day
    $link->query("DELETE FROM " . Config::$Activations_Tablename . " WHERE `timestamp_created` < NOW() - INTERVAL 1 DAY");
    
    // purge user records that haven't been activated for over a week
    $link->query("DELETE FROM " . Config::$Users_Tablename . " WHERE activated = 'N' AND `joining_date` < NOW() - INTERVAL 1 WEEK");
    
    // purge log records that are older than 25 hours
    $link->query("DELETE FROM " . Config::$Logs_Tablename . " WHERE row_timestamp < NOW() - INTERVAL 25 HOUR");
    
    // purge one-time anti-replay tokens older than 30 minutes
    $link->query("DELETE FROM " . Config::$Replay_Tablename . " WHERE token_timestamp < NOW() - INTERVAL 30 MINUTE");
    
    return true;
}

At 4/9/16 09:23 AM, NinoGrounds wrote: I am not using any API.

I was referring to all the calls to static methods (e.g. Config, Utils, NAuthException). After skimming more of your code while writing this, it appears those are your own API.

At 4/9/16 09:23 AM, NinoGrounds wrote: I wasn't aiming it at being MVP compliant, but there's a strict separation of code and presentation.

That's really what the whole point of MVC is. Based on what you've said, it sounds like you're currently using controllers and views, but not models. All you would need to do to make it MVC-compliant would be to move your data manipulation out of your controllers and put them in models (controllers should only handle requests/responses, validate data, and communicate with the models and views).

At 4/9/16 09:23 AM, NinoGrounds wrote: It's a garbage collection bit, and PHP uses the same thing internally for its garbage collection. Basically, what it does is, every 100 times (on average), it's gonna trigger the database GC method.

So far as I know, what you're referring to is for cleaning up sessions. And unless PHP is doing yet another retarded thing that doesn't make sense—which wouldn't surprise me—1/100 is never going to equal 50, and especially not so with the strict equality operator.

I don't know what Codecanyon's standards are, but this not being MVC and having everything be static members in classes are red flags to me. If everything is static members and static methods inside classes, what's the point of using classes?

This also doesn't seem to support proper URI routing, which would be another red flag. (Which means this can't be used to create RESTful applications.) Creating routes shouldn't require having to create whole new PHP scripts which are mapped 1:1 to the drive that they are hosted on; it should be done something like this:

<?php

Route::get("/", "HomeController@index");

Route::get("/user/{username}", "UserController@profile");

Route::get("/login", "UserController@login");
Route::post("/login", "UserController@handleLogin");

This is how Laravel and Flask work, and pretty much any framework worth its salt, and it's a very powerful design pattern.

There's no reason to publicly expose the file structure of your server or to be includind the .php extension in your URIs, or to be using completely separate files for handling POST requests (e.g. log-in-process.php). Having different files for different request methods defeats the purpose of HTTP: the fact that a different request method is being used is all you need to know about how to handle the login route (which is what RESTful is all about).

I can't say with any certainty that those are some of the reasons that this was denied, but they're definitely things I would fix.

edit:

I also can't say I'm fond of your demo forcing JavaScript usage for no good reason. I use NoScript, which means by default the demo is bricked because of your .container element having an opacity of 0, which is reverted via JS. The site (mostly) functions perfectly fine without JS and with this Greasemonkey/Tampermonkey script to revert the opacity:

// ==UserScript==
// @name        NAuth
// @match       http://nauthentication.recgr.com/*
// @grant       none
// ==/UserScript==

document.getElementsByClassName("container")[0].style.opacity = 100;

Response to Project got declined on Codecanyon 2016-04-19 02:15:03


Diki,

I thank you so much for taking the time to give me the amazing feedback. I saw it on the first day when you posted it, but at the moment I'm working on something else, so I can't really do all those things, but I def will once I'm done with the current client.

Once again, thanks so much. Also, I just released a new blog, so feel free to check that out. I have a pretty big list of features I wanna add, but first I gotta see if I get any traction on it at all. I thought I had a pretty good marketing technique - installing dating apps (Tinder etc), and put the link in the profile description. It ended up being a complete waste of time :P