Skip to content

Tutorial A Simple Login System

Xyranaut edited this page Jun 1, 2026 · 3 revisions

Tutorial: a simple login system from scratch

This builds a minimal but complete register/login system step by step, so you see how every piece fits. It's deliberately smaller than the full mysql-admin demo — once you understand this, that demo will make sense.

You'll need: a running MySQL (Installing MySQL), the component installed, and a DB user (Getting started).

We'll build it in 6 steps. Copy each block into a filterscript (e.g. mylogin.pwn).


Step 1 — includes and globals

#include <open.mp>
#include <omp-mysql>

#define DB_HOST "127.0.0.1"
#define DB_USER "omp_app"
#define DB_NAME "mydb"

new MySQL:g_DB;
new bool:g_LoggedIn[MAX_PLAYERS];   // is this player authenticated?
new g_AccountId[MAX_PLAYERS];       // their DB row id

#define DIALOG_LOGIN    1
#define DIALOG_REGISTER 2

Step 2 — connect and make the table

public OnFilterScriptInit()
{
    new MySQLConfig:cfg = mysql_config_create();
    mysql_config_set(cfg, SSL_MODE, SSL_MODE_REQUIRED);   // TLS on
    g_DB = mysql_connect(DB_HOST, DB_USER, "${OMP_DB_PASS}", DB_NAME, cfg);
    if (g_DB == MYSQL_INVALID_HANDLE)
    {
        print("[mylogin] DB connect failed");
        return 1;
    }
    mysql_execute_sync(g_DB,
        "CREATE TABLE IF NOT EXISTS users (\
         id INT PRIMARY KEY AUTO_INCREMENT, \
         name VARCHAR(24) NOT NULL UNIQUE, \
         hash VARCHAR(255) NOT NULL)");
    print("[mylogin] ready");
    return 1;
}

Set the password in your environment before starting the server: export OMP_DB_PASS="your-password".

Step 3 — on join, look up the account

public OnPlayerConnect(playerid)
{
    g_LoggedIn[playerid] = false;
    g_AccountId[playerid] = 0;

    new name[MAX_PLAYER_NAME];
    GetPlayerName(playerid, name, sizeof name);

    // prepared statement = safe even if the name has weird characters
    new PreparedStatement:st = mysql_prepare(g_DB,
        "SELECT id, hash FROM users WHERE name = ? LIMIT 1");
    mysql_stmt_set_string(st, 1, name);
    mysql_stmt_execute(st, "OnLookup", "d", playerid);
    return 1;
}

Step 4 — show Login or Register

forward OnLookup(playerid);
public OnLookup(playerid)
{
    new rows; mysql_rs_row_count(rows);
    if (rows > 0)
    {
        // account exists -> remember its id and ask for the password
        mysql_rs_get_int_by(0, "id", g_AccountId[playerid]);
        ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD,
            "Login", "Welcome back. Enter your password:", "Login", "Quit");
    }
    else
    {
        ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_PASSWORD,
            "Register", "New here. Choose a password:", "Register", "Quit");
    }
    return 1;
}

Step 5 — handle the dialog (register = hash; login = verify)

public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[])
{
    if (dialogid == DIALOG_REGISTER)
    {
        if (!response) { Kick(playerid); return 1; }       // ESC = leave
        if (strlen(inputtext) < 4)
        {
            ShowPlayerDialog(playerid, DIALOG_REGISTER, DIALOG_STYLE_PASSWORD,
                "Register", "Too short (4+ chars):", "Register", "Quit");
            return 1;
        }
        // hash the password (Argon2id) off the main thread, then insert
        mysql_hash(inputtext, "OnHashed", "d", HASH_ARGON2ID, playerid);
        return 1;
    }
    if (dialogid == DIALOG_LOGIN)
    {
        if (!response) { Kick(playerid); return 1; }
        SetPVarString(playerid, "try_pw", inputtext);
        // load the stored hash, then verify in OnVerifyLoaded
        new PreparedStatement:st = mysql_prepare(g_DB,
            "SELECT hash FROM users WHERE id = ?");
        mysql_stmt_set_int(st, 1, g_AccountId[playerid]);
        mysql_stmt_execute(st, "OnVerifyLoaded", "d", playerid);
        return 1;
    }
    return 0;
}

Step 6 — finish register and login

// REGISTER: hash is ready -> INSERT the new account
forward OnHashed(playerid, const hash[]);
public OnHashed(playerid, const hash[])
{
    new name[MAX_PLAYER_NAME];
    GetPlayerName(playerid, name, sizeof name);
    new PreparedStatement:st = mysql_prepare(g_DB,
        "INSERT INTO users (name, hash) VALUES (?, ?)");
    mysql_stmt_set_string(st, 1, name);
    mysql_stmt_set_string(st, 2, hash);
    mysql_stmt_execute(st, "OnRegistered", "d", playerid);
    return 1;
}

forward OnRegistered(playerid);
public OnRegistered(playerid)
{
    g_AccountId[playerid] = mysql_rs_insert_id();
    g_LoggedIn[playerid] = true;
    SendClientMessage(playerid, 0x66FF66FF, "Registered and logged in!");
    return 1;
}

// LOGIN: stored hash loaded -> verify the password the player typed
forward OnVerifyLoaded(playerid);
public OnVerifyLoaded(playerid)
{
    new hash[255], pw[64];
    mysql_rs_get_string_by(0, "hash", hash, sizeof hash);
    GetPVarString(playerid, "try_pw", pw, sizeof pw);
    DeletePVar(playerid, "try_pw");

    if (mysql_verify_sync(pw, hash))
    {
        g_LoggedIn[playerid] = true;
        SendClientMessage(playerid, 0x66FF66FF, "Logged in!");
    }
    else
    {
        SendClientMessage(playerid, 0xFF6666FF, "Wrong password.");
        ShowPlayerDialog(playerid, DIALOG_LOGIN, DIALOG_STYLE_PASSWORD,
            "Login", "Wrong password. Try again:", "Login", "Quit");
    }
    return 1;
}

That's a complete login system

What you just used:

  • TLS connection (mandatory),
  • a prepared statement for the lookup, login load, and INSERT (injection-safe),
  • Argon2id hashing (mysql_hash) and verification (mysql_verify_sync),
  • callbacks to stay async (no server lag),
  • cancel-the-dialog = kicked (so login is mandatory).

Make it production-ready

  • Wipe g_LoggedIn[playerid] on OnPlayerDisconnect (so a reused slot isn't auto-logged-in).
  • Block commands/actions until g_LoggedIn[playerid] is true; add a login timeout.
  • Use SSL_MODE_VERIFY_CA with a real CA in production.
  • See the full mysql-admin demo for all of this done properly, plus saving position/money/etc., admin levels, and RCON tools.

Stuck? See Troubleshooting.

Clone this wiki locally