[Tutorial] Register/Login System Using DJson *Includes DUDB Conversion!
#1

[Tutorial] Register/Login System Using DJson, DCMD, and Whirlpool password encryption! *Includes DUDB Conversion!
Difficulty: Pretty Easy - Some basic scripting knowledge is helpful.
NOTE: You are only hurting yourself by just copy and pasting these scripts, you will be confused and not know what do to. I will NOT provide ANY support on this if you haven't first read the topic. It really, really, doesn't get much simpler than this. I've explained everything out for you. Read this tutorial so you can actually learn some things. You won't get anywhere by copy and pasting and than telling all your buddies that you made it, even though you didn't do anything.

A few notes before beggining:
A string is a linear sequence of symbols (characters or words or phrases) Basically just text.
An Int(Integer) is any of the natural numbers (positive or negative) or zero, an integer is a number that is not a fraction.


I know I've probably made some speling errors and mistakes, please point them out to me so I can fix them. Nobody's perfect :P


I've always used DUDB for my user-files and such and when DJson came out I couldn't really understand it and I though since I've always used DUDB, it would take AGES of time to convert all the code. One day I saw a Account System include called Cez-Account. I downloaded it but still had the problem with the DUDB stuff. A few days ago I went back to look at it and added a simple fix to convert every DUDB line to work with the include/DJson. I made some little changes to the include and started making a register & login script.

So today I will make a tutorial on how to create a Register/Login script using DJson, and that include.
You can find the original cez-account include topic here: http://forum.sa-mp.com/index.php?topic=84762.0
But I suggest you use mine since that is what I will be using in this tutorial.

1. First open up pawno to a blank screen. Copy the code below, paste it into your blank pawno script. Click File -> Save as and locate to your \pawno\includes folder. Save it with the name, 'ACC.inc'. You do not need to press compile, all you need to do is save it to your includes folder. Also, the tabs have been replaced with the correct amount of spaces, meaning you should be able to read and copy/paste it perfectly.
pawn Код:
/*
#        Cez-Account v1.0
#    © Copyright 2008 - 2009 Cezar Mihail Ignat
#
#  Requires: DracoBlue's DJson - © Copyright 2008 DracoBlue
#        http://cgarage.blogspot.com/
#
*/

// Small modifications by Lavamike


#if defined _cezacc_included
 #endinput
#endif

#define _cezacc_included

#include <djson>

// DUDB Conversions
#define dUser(%1).( AccountGet(%1,
#define dUserINT(%1).( AccountGetInt(%1,
#define dUserSet(%1).( AccountSet(%1,
#define dUserSetINT(%1).( AccountSetInt(%1,
#define dUserSetFLOAT(%1).( AccountSetFloat(%1,
#define dUserFLOAT(%1).( AccountGetFloat(%1,
#define dUserIsSet(%1).( AccountIsSet(%1,
#define udb_Exists(%1) AccountExists(%1)

// Whirlpool for SA:MP by Y-Less
// http://forum.sa-mp.com/index.php?topic=89418.0
native WP_Hash(buffer[], len, const str[]);

stock num_hash(buf[]) {
  new length=strlen(buf);
  new s1 = 1;
  new s2 = 0;
  new n;
  for (n=0; n<length; n++) {
   s1 = (s1 + buf[n]) % 65521;
   s2 = (s2 + s1)  % 65521;
  }
  return (s2 << 16) + s1;
}

stock AccountExists(nickname[]) {
 return djIsSet("accounts.json",nickname,false);
}


stock AccountRemove(nickname[]) {
 return djUnset("accounts.json",nickname);
}

stock AccountSetInt(nickname[],key[],value) {
 if(!AccountExists(nickname)) return false;
 new str[255];
 format(str, sizeof(str), "%s/%s", nickname, key);
 return djSetInt("accounts.json", str, value);
}

stock AccountSetFloat(nickname[],key[],Float:value) {
 if(!AccountExists(nickname)) return false;
 new str[255];
 format(str, sizeof(str), "%s/%s", nickname, key);
 return djSetFloat("accounts.json", str, value);
}

stock AccountSet(nickname[],key[],value[]) {
 if(!AccountExists(nickname)) return false;
 new str[255];
 format(str, sizeof(str), "%s/%s", nickname, key);
 return djSet("accounts.json", str, value);
}

stock AccountGet(nickname[],key[]) {
 new str[255];
 format(str, sizeof(str), "%s/%s", nickname, key);
 return dj("accounts.json", str);
}

stock Float:AccountGetFloat(nickname[],key[]) {
 new str[255];
 format(str, sizeof(str), "%s/%s", nickname, key);
 return djFloat("accounts.json",str);
}

stock AccountGetInt(nickname[],key[]) {
 new str[255];
 format(str, sizeof(str), "%s/%s", nickname, key);
 return djInt("accounts.json",str);
}

stock AccountCheckLogin(nickname[],pwd[]) {
 new str[255];
 format(str, sizeof(str), "%s/password", nickname);
 if (djInt("accounts.json",str) == num_hash(pwd)) return true;
 return false;
}

stock AccountCreate(nickname[],pwd[]) {
 if (AccountExists(nickname)) return false;
 new str[31];
 format(str, sizeof(str), "%s/password", nickname);
 new buf[145];
 WP_Hash(buf, sizeof(buf), pwd);
 djSet("accounts.json",str,buf);
 printf("Account Created: %s - %s\n%s", nickname, pwd, buf);
 return true;
}

// I'm pretty sure I added this into the DUDB include a while ago.
// I just added and converted it here since I used it in my script :P
// It just checks if a key has something set to it or not.
stock AccountIsSet(nickname[],key[]) {
 new str[255];
 format(str, sizeof(str), "%s/%s", nickname, key);
 return djIsSet("accounts.json",str);
}

2. Second, we need to get the Whirlpool plugin by Y-Less. Get the appropriate version here:
http://forum.sa-mp.com/index.php?topic=89418.0
If you do not wish to use Whirlpool than comment out: native WP_Hash(buffer[], len, const str[]); in the include!
For this tutorial I am using Whirlpool for password encryption, if you want to use something else than you need to figure out how to use it. Do NOT post here asking me how to implement a different password encryption system please.

Once you have downloaded the Whirlpool plugin, put it into your \plugins\ folder. (If you do not have a plugins folder create it with that exact name, 'plugins'.)

Now, go to your server.cfg and add this: plugins Whirlpool.dll on a separate line. (.dll is for windows, i'm not familiar with how to do it on Linux, sorry ) If you have other plugins just add Whirlpool.dll on to that list, example: plugins YourPlugin1.dll YourPlugin2.dll Whirlpool.dll


3. Okay now it's time to get into the actualy scripting of the command using our new include and Whirlpool plugin.



Scripting

Alright, lets deal with /register first. Here's some information. The way we will be doing this is we have one file called accounts.json Inside there we have a category for each person's name aka each name that is registered with your server. Inside each person's 'category' we have all of there information saved. DJson will put all of the information in alphabetical order also.


P.S: This diagram took me like 45 minutes to get right, so you better enjoy it!

Okay, now lets get onto the pawn scripting part. For this tutorial I will be using DCMD.
If you are unfamiliar with DCMD, there is a tutorial about it here: http://forum.sa-mp.com/index.php?topic=75000.0

First we must change our includes and add 2 defines. If you use DUDB, comment or delete the: #include <DUDB>

At the top of your script add:

#include <ACC>
#define COLOR_SUCCESS 0x64F600FF
#define COLOR_RED 0xFF0000FF

You can change those colors to whatever you want, just have them defined, or change it in the script.

Now we start with the register command. We add it into OnPlayerCommandText:
pawn Код:
dcmd(register,8,cmdtext);
Now we make our basic command function here.
pawn Код:
dcmd_register(playerid, params[])
{
  return 1;
}
The first thing we will do is check if their account already exists. To do this we will get the name of the player and use AccountExists();
pawn Код:
dcmd_register(playerid, params[])
{
  new PlayerName[24]; // Create an array with the size of 24 to store the name in.
  GetPlayerName(playerid, PlayerName, 24); // Store the name to the PlayerName array.
  if(!AccountExists(PlayerName)) // Check if the account exists.
  {
   
  }
  else if(AccountGetInt(PlayerName, "LoggedIn") == 0) return SendClientMessage(playerid, COLOR_RED, "Account already exists. Please use /login password to login.");
  else return SendClientMessage(playerid, COLOR_RED, "You are already logged on!");
  return 1;
}
If you are confused on what we did here read on.
First, we create a spot to put the players current username. We then get the name and store it in that spot. We ask the server if the account doesn't exist, than continue. Otherwise it will go down to the end of the } and to the else's. Some people get confused with the else's. Basically all we are doing is if it finds the account does exist, it will skip down there for further instruction. The first one says if they are not logged on, (the vlaue "LoggedOn" we will set later) send them a message saying their account already exists and to use /login password to login to their account. Than the second else is called if the first one isn't true. Basically it means if they are logged on. In order to be logged on they must have an account already so it just tells them that they are already logged on.

Now let's move onto the core:
First we will check a few things on their password..

pawn Код:
dcmd_register(playerid, params[])
{
  new PlayerName[24]; // Create an array with the size of 24 to store the name in.
  GetPlayerName(playerid, PlayerName, 24); // Store the name to the PlayerName array.
  if(!AccountExists(PlayerName)) // Check if the account exists.
  {
    if(!strlen(params)) return SendClientMessage(playerid, COLOR_RED, "/register password [Password is CaSE SeNSiTivE]");
    if(strlen(params) > 20) return SendClientMessage(playerid, COLOR_RED, "Password cannot be greater than 20 characters.");
    if(strlen(params) < 3) return SendClientMessage(playerid, COLOR_RED, "Password cannot be less than 3 characters.");
  }
  else if(AccountGetInt(PlayerName, "LoggedIn") == 0) return SendClientMessage(playerid, COLOR_RED, "Account already exists. Please use /login password to login.");
  else return SendClientMessage(playerid, COLOR_RED, "You are already logged on!");
  return 1;
}
Okay now as you can see we've added 3 if's. First of all, strlen stands for, "string length". Params is the information submitted by the user after, '/register ' which means if I were to type in /register ThisIsMyPass, params would be, "ThisIsMyPass". So here we check if they submit any information. If they didn't submit any information as in they just typed /register without anything else, it tells them the syntax of the command and that the password is Case Sensitive. Case Sensitive means capital letters make a difference. Meaning, If I do /register Hello and try to do /login hello it would say my password was wrong because I used a lowercase h. The second if checks if the length of params is over 20. If it is we tell them their password cannot be over 20 characters long. The third checks if the length of params is less than 3. If it is we tell them their password cannot be less than 3 characters long.


Alright, so now we are ready to create their account with their password and set some data.

pawn Код:
dcmd_register(playerid, params[])
{
  new PlayerName[24]; // Create an array with the size of 24 to store the name in.
  GetPlayerName(playerid, PlayerName, 24); // Store the name to the PlayerName array.
  if(!AccountExists(PlayerName)) // Check if the account exists.
  {
    if(!strlen(params)) return SendClientMessage(playerid, COLOR_RED, "/register password [Password is CaSE SeNSiTivE]");
    if(strlen(params) > 20) return SendClientMessage(playerid, COLOR_RED, "Password cannot be greater than 20 characters.");
    if(strlen(params) < 3) return SendClientMessage(playerid, COLOR_RED, "Password cannot be less than 3 characters.");
    new str[31];
    format(str, sizeof(str), "%s/password", PlayerName);
    new buf[145];
    WP_Hash(buf, sizeof(buf), params);
    djSet("accounts.json",str,buf);
    AccountSetInt(PlayerName, "LoggedIn",0);
    AccountSet(PlayerName, "password", buf);
    printf("[REGISTER] %s - %s [%s]",PlayerName, IP, DayString);
    SendClientMessage(playerid, COLOR_SUCCESS, "You have successfully registered! You may now use /login password to login.");
  }
  else if(AccountGetInt(PlayerName, "LoggedIn") == 0) return SendClientMessage(playerid, COLOR_RED, "Account already exists. Please use /login password to login.");
  else return SendClientMessage(playerid, COLOR_RED, "You are already logged on!");
  return 1;
}
Alright, now we have some pretty lengthy code :P

Let's take a look and see what we are doing here.

First, we create a spot to store something for DJson. For this i'm using the actual DJson functions, not the include functions. str is just short for string here. Now we format some text into here. What you see here is %s/password %s stands for string, which means we are putting a string into there. In this case it's the name of the player. As seen in the diagram above, we submit the PlayerName(username) as the first category, than the key is password which we set in a moment. So now we have the location ready. Now we will take the password they submitted into the command and encrypt it into a cryptographic code. In layman's terms: a secret code that nobody can read. We create a buf with the length of 145. 145 is the minimum amount that Whirlpool. Whenever using the Whirlpool for passwords in your script you should keep it consistent. I suggest just using 145. Buf is just what we are storing the secret code to. WP_Hash is a function included with ******'s Whirlpool plugin, used to encrypt text. In this case our text is the information they submitted which we now know is stored in params.

Now we will set the password in their account, BUT WAIT! We never did any Create account thingy!! Don't worry, DJson will automatically create it. So now we have set the password to their account. Now we set two more things, the LoggedIn value, which we set to 0, and I set the password again to make sure it was set. And we send a message to them saying they have successfully been registered and that you can now use /login to login.


Alright, were done with the registration command, bathroom break!




Continued below because I couldn't fit everything within the 20000 character limit
Reply
#2

Okay, now it's time to make /login.
Add the login command in OnPlayerCommandText:
pawn Код:
dcmd(login,5,cmdtext);
Now we start how we did with /register
pawn Код:
dcmd_login(playerid, params[])
{
  new PlayerName[24];
  GetPlayerName(playerid, PlayerName, 24);
  if(AccountExists(PlayerName))
  {
    if(AccountGetInt(PlayerName, "LoggedIn") == 1) return SendClientMessage(playerid, COLOR_RED, "You are already logged in!");
    if(!strlen(params)) return SendClientMessage(playerid, COLOR_RED, "/login password [Password is CaSE SeNSiTivE]");
    if(strlen(params) > 20) return SendClientMessage(playerid, COLOR_RED, "Password cannot be greater than 20 characters.");
    if(strlen(params) < 3) return SendClientMessage(playerid, COLOR_RED, "Password cannot be less than 3 characters.");
  } else return SendClientMessage(playerid, COLOR_RED, "Account does not exist. Please use /register password.");
  return 1;
}
Alright, now what we have here is getting the PlayerName once again and checking if the account exists. This time we have an if saying if the account does exist, continue. Otherwise, skip down to the bottom else and tell them that their account doesn't exist and that they need to use /register first. So if their account does exist we run a few checks. The 2nd, 3rd, and 4th we are already familiar with from /register. But the first one, even though we used it at one point you may not recognize. We are simply checking if they are logged in or not and if they are we tell them that they are already logged in.

Now lets add the password check:
pawn Код:
dcmd_login(playerid, params[])
{
  new PlayerName[24];
  GetPlayerName(playerid, PlayerName, 24);
  if(AccountExists(PlayerName))
  {
    if(AccountGetInt(PlayerName, "LoggedIn") == 1) return SendClientMessage(playerid, COLOR_RED, "You are already logged in!");
    if(!strlen(params)) return SendClientMessage(playerid, COLOR_RED, "/login password [Password is CaSE SeNSiTivE]");
    if(strlen(params) > 20) return SendClientMessage(playerid, COLOR_RED, "Password cannot be greater than 20 characters.");
    if(strlen(params) < 3) return SendClientMessage(playerid, COLOR_RED, "Password cannot be less than 3 characters.");
    new buf[145];
    WP_Hash(buf, sizeof(buf), params);
    new PwCheck[145];
    new str[35];
    format(str, sizeof(str), "%s/password", PlayerName);
    format(PwCheck, sizeof(PwCheck), "%s", dj("accounts.json", str));
    if(strcmp(PwCheck, buf, false) == 0)
    {
     
    }
  } else return SendClientMessage(playerid, COLOR_RED, "Account does not exist. Please use /register password.");
  return 1;
}
Okay, since our 'secret code' cannot be decrypted, meaning we cannot turn it back into normal text, we take the params they enter in /login and convert that into the secret code format. Than we compare the password they entered to the one stored on file. If they match, let them login, otherwise, they have the invalid password.

So, we encrypt the params they enter just like in /register, than we create another format which has that %s/password again. That is our file-password location. We use this in our next new format. We called it PwCheck. Notice the size is 145, the same as what we used for our buf in /register and right in login here. Remember, I said you should keep it consistent? Alrighty, now as you can see all that's in the format is %s. Remember what that is? It means we are replacing it for a string. In this case we are getting the password stored on file and replacing it there. So now we have both secret password codes stored. The one submitted in /login is stored in buf, and the one on file is stored as PwCheck. Now we have a new if. I starts with strcmp? What the heck is that? strcmp stands for, "String Compare". Right, so now the pieces of the puzzle all fit in. We are comparing the two strings(the two encrypted passwords) to see if they match. If they match it will = 0. So if it equals 0 (matches) we continue.

pawn Код:
dcmd_login(playerid, params[])
{
  new PlayerName[24];
  GetPlayerName(playerid, PlayerName, 24);
  if(AccountExists(PlayerName))
  {
    if(AccountGetInt(PlayerName, "LoggedIn") == 1) return SendClientMessage(playerid, COLOR_RED, "You are already logged in!");
    if(!strlen(params)) return SendClientMessage(playerid, COLOR_RED, "/login password [Password is CaSE SeNSiTivE]");
    if(strlen(params) > 20) return SendClientMessage(playerid, COLOR_RED, "Password cannot be greater than 20 characters.");
    if(strlen(params) < 3) return SendClientMessage(playerid, COLOR_RED, "Password cannot be less than 3 characters.");
    new buf[145];
    WP_Hash(buf, sizeof(buf), params);
    new PwCheck[145];
    new str[35];
    format(str, sizeof(str), "%s/password", PlayerName);
    format(PwCheck, sizeof(PwCheck), "%s", dj("accounts.json", str));
    if(strcmp(PwCheck, buf, false) == 0)
    {
      AccountSetInt(PlayerName, "LoggedIn", 1);
      SendClientMessage(playerid, COLOR_SUCCESS, "You have successfully logged in!");
    }
  } else return SendClientMessage(playerid, COLOR_RED, "Account does not exist. Please use /register password.");
  return 1;
}
So now we simply set their LoggedIn value to 1, meaning they have logged in successfully and tell them they have logged in successfully.


- Credits -
-Lavamike [Making the tutorial]
-Dracoblue [Making DJson & DUDB and all your other useful things]
-Cez/Cezar [Making Cez-ACC]
-****** [Making the Whirlpool plugin]
If I missed someone please let me know


I hope some people find it useful!
-Mike
Reply
#3

Nice! finaly someone who used the size of arrays/strings correctly! well done!
Reply
#4

Thanks for this nice tutorial (I added link to the initial djson topic)!

One thing I would change is the style of one big file for all data. Even though djson is very fast it has to save the entire file down to disk, as soon as you call djCommit (which is implicit by dj*Set if you didn't disabled autocommit). Given this, it will take like a second for 1000 accounts and memory usage, your script will hang then.

Since djson's caching works for multiple files as fast as for single files, you will even expirience a load speed and if the community grows bigger a huge write performance boost, if you create one file for each player.

I know, some like the one way more, others the other way. In case of a performance driven account system, I would go for an extra folder with one file for each player. Discussion is of course welcome!

Have fun coding,
Draco
Reply
#5

Some good points, I will edit the topic, most likely tomorrow(in my timezone(EST)) Maybe I can fit it somewhere in between the two posts without going over the 20k text limit :P


Now I have to make another tree diagram :P
Reply
#6

Quote:
Originally Posted by Lavamike
Now I have to make another tree diagram :P
Good luck!

I noticed that http://dev.dracoblue.net/index.php/DJson was down, so I made it working again, maybe we can add the tutorial to these pages, too? Would be nice introduction in a case for using DJson (and has no character limit ^^)!

- Draco
Reply
#7

Quote:
Originally Posted by DracoBlue
Quote:
Originally Posted by Lavamike
Now I have to make another tree diagram :P
Good luck!

I noticed that http://dev.dracoblue.net/index.php/DJson was down, so I made it working again, maybe we can add the tutorial to these pages, too? Would be nice introduction in a case for using DJson (and has no character limit ^^)!

- Draco
Sure, but i'm having a little trouble with the multiple files with that printing out (null) again even though I've checked that error i made last time and it seems to be looking normal. And It finds the value i'm looking for if i use that DJSON_cache_debug_print or whatever it is. I'll try and work on it now as I took a break to clear my mind out, maybe i'll find the error now.

Yeah, I kept forgetting to tell you it was down, maybe it will be of use to me now in whatever error i'm making.

Could you post a small example on how to use dj() & djSetInt() with the json file being in a folder? That would be helpful, Thanks.

-Mike
Reply
#8

Quote:
Originally Posted by Lavamike
Could you post a small example on how to use dj() & djSetInt() with the json file being in a folder? That would be helpful, Thanks.
Setting:
pawn Код:
new fname[50];
GetPlayerName(playerid, fname, 50);
format(fname,50,"\\accounts\\%s.json",fname);
djSetInt(fname,"money",120) // writes the value money to a file called at scriptfiles/accounts/DracoBlue.json which could look like that:
{
 "money": 120
}
Getting:
pawn Код:
new fname[50];
GetPlayerName(playerid, fname, 50);
format(fname,50,"\\accounts\\%s.json",fname);
dj(fname,"money") // reads 120
Hope you get an idea!

- Draco
Reply
#9

I'm still having some troubles even doing it like you said there. It's very weird, I've sent you a PM with some information.
Reply
#10

pawn Код:
new fname[50];
GetPlayerName(playerid, fname, 50);
format(fname,50,"\\accounts\\%s.json",fname);
dj(fname,"money") // reads 120
why do u use \\?

double slashes...

whats the difference.. for me it works with single backslash...

srry for bump
Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)