666 lines
22 KiB
JavaScript
666 lines
22 KiB
JavaScript
/**
|
|
* Tekton Nakama Server Runtime Module
|
|
*
|
|
* This module provides secure admin operations via RPC calls.
|
|
* Deploy this to your Nakama server's runtime directory.
|
|
*/
|
|
|
|
// Initialize RPC endpoints
|
|
function InitModule(ctx, logger, nk, initializer) {
|
|
// Admin RPCs
|
|
initializer.registerRpc("admin_kick_player", rpcAdminKickPlayer);
|
|
initializer.registerRpc("admin_ban_player", rpcAdminBanPlayer);
|
|
initializer.registerRpc("admin_unban_player", rpcAdminUnbanPlayer);
|
|
initializer.registerRpc("admin_get_ban_list", rpcAdminGetBanList);
|
|
initializer.registerRpc("admin_get_server_stats", rpcAdminGetServerStats);
|
|
initializer.registerRpc("admin_get_player_list", rpcAdminGetPlayerList);
|
|
initializer.registerRpc("admin_end_match", rpcAdminEndMatch);
|
|
initializer.registerRpc("admin_set_user_role", rpcAdminSetUserRole);
|
|
initializer.registerRpc("admin_list_users", rpcAdminListUsers);
|
|
initializer.registerRpc("admin_delete_users", rpcAdminDeleteUsers);
|
|
|
|
// User management RPCs
|
|
initializer.registerRpc("get_user_profile", rpcGetUserProfile);
|
|
initializer.registerRpc("update_user_profile", rpcUpdateUserProfile);
|
|
|
|
// Leaderboard RPCs
|
|
initializer.registerRpc("get_leaderboard_stats", rpcGetLeaderboardStats);
|
|
initializer.registerRpc("admin_update_stats", rpcAdminUpdateStats);
|
|
initializer.registerRpc("admin_delete_stats", rpcAdminDeleteStats);
|
|
initializer.registerRpc("admin_sync_leaderboard", rpcAdminSyncLeaderboard);
|
|
|
|
// Create default native leaderboard
|
|
// id: "global_high_score", authoritative: true, sort: "desc", operator: "best", reset: None
|
|
nk.leaderboardCreate("global_high_score", true, "desc", "best", null, {});
|
|
|
|
logger.info("Tekton admin module loaded");
|
|
}
|
|
|
|
// =============================================================================
|
|
// Authorization Helpers
|
|
// =============================================================================
|
|
|
|
var ADMIN_ROLES = ["admin", "moderator", "owner"];
|
|
|
|
function isAdmin(ctx, nk) {
|
|
if (!ctx.userId) return false;
|
|
|
|
try {
|
|
var account = nk.accountGetId(ctx.userId);
|
|
var metadata;
|
|
if (typeof account.user.metadata === "string") {
|
|
metadata = JSON.parse(account.user.metadata || "{}");
|
|
} else {
|
|
metadata = account.user.metadata || {};
|
|
}
|
|
var role = metadata.role || "";
|
|
return ADMIN_ROLES.indexOf(role) !== -1;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isMatchHost(ctx, nk, matchId) {
|
|
if (!ctx.userId || !matchId) return false;
|
|
|
|
try {
|
|
// Get match state to check host
|
|
var match = nk.matchGet(matchId);
|
|
if (!match) return false;
|
|
|
|
// The first user to join (presence) is typically the host
|
|
// This logic may need adjustment based on your match handler
|
|
var state = JSON.parse(match.state || "{}");
|
|
return state.hostUserId === ctx.userId;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function requireAdmin(ctx, nk) {
|
|
if (!isAdmin(ctx, nk)) {
|
|
throw new Error("Admin privileges required");
|
|
}
|
|
}
|
|
|
|
function requireAdminOrHost(ctx, nk, matchId) {
|
|
if (!isAdmin(ctx, nk) && !isMatchHost(ctx, nk, matchId)) {
|
|
throw new Error("Admin or host privileges required");
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Admin RPCs
|
|
// =============================================================================
|
|
|
|
function rpcAdminKickPlayer(ctx, logger, nk, payload) {
|
|
var request = JSON.parse(payload);
|
|
|
|
requireAdminOrHost(ctx, nk, request.match_id);
|
|
|
|
// Can't kick yourself
|
|
if (request.user_id === ctx.userId) {
|
|
throw new Error("Cannot kick yourself");
|
|
}
|
|
|
|
try {
|
|
// Signal the match to kick the player
|
|
nk.matchSignal(request.match_id, JSON.stringify({
|
|
action: "kick",
|
|
user_id: request.user_id,
|
|
reason: request.reason || "Kicked by admin"
|
|
}));
|
|
|
|
logger.info("Player " + request.user_id + " kicked from match " + request.match_id + " by " + ctx.userId);
|
|
|
|
return JSON.stringify({ success: true });
|
|
} catch (e) {
|
|
logger.error("Failed to kick player: " + e);
|
|
throw new Error("Failed to kick player");
|
|
}
|
|
}
|
|
|
|
function rpcAdminBanPlayer(ctx, logger, nk, payload) {
|
|
var request = JSON.parse(payload);
|
|
|
|
// Only full admins can ban (not just match hosts)
|
|
requireAdmin(ctx, nk);
|
|
|
|
if (request.user_id === ctx.userId) {
|
|
throw new Error("Cannot ban yourself");
|
|
}
|
|
|
|
try {
|
|
// Get target user's account
|
|
var targetAccount = nk.accountGetId(request.user_id);
|
|
var metadata = JSON.parse(targetAccount.user.metadata || "{}");
|
|
|
|
// Don't allow banning other admins
|
|
if (ADMIN_ROLES.indexOf(metadata.role || "") !== -1) {
|
|
throw new Error("Cannot ban an admin");
|
|
}
|
|
|
|
// Set ban in metadata
|
|
var banExpires = request.duration_hours && request.duration_hours > 0
|
|
? new Date(Date.now() + request.duration_hours * 60 * 60 * 1000).toISOString()
|
|
: null;
|
|
|
|
metadata.banned = true;
|
|
metadata.ban_reason = request.reason || "Banned by admin";
|
|
if (banExpires) {
|
|
metadata.ban_expires = banExpires;
|
|
}
|
|
|
|
nk.accountUpdateId(request.user_id, null, null, null, null, null, null, JSON.stringify(metadata));
|
|
|
|
// Also kick from current match if specified
|
|
if (request.match_id) {
|
|
nk.matchSignal(request.match_id, JSON.stringify({
|
|
action: "kick",
|
|
user_id: request.user_id,
|
|
reason: "Banned: " + (request.reason || "")
|
|
}));
|
|
}
|
|
|
|
// Store in ban list (for quick lookup)
|
|
var banRecord = {
|
|
user_id: request.user_id,
|
|
username: targetAccount.user.username,
|
|
banned_by: ctx.userId,
|
|
banned_at: new Date().toISOString(),
|
|
reason: request.reason,
|
|
expires: banExpires
|
|
};
|
|
|
|
nk.storageWrite([{
|
|
collection: "bans",
|
|
key: request.user_id,
|
|
userId: "00000000-0000-0000-0000-000000000000", // System-owned
|
|
value: banRecord,
|
|
permissionRead: 2, // Public read
|
|
permissionWrite: 0 // No one can write (except server)
|
|
}]);
|
|
|
|
logger.warn("Player " + request.user_id + " banned by " + ctx.userId + ". Reason: " + request.reason);
|
|
|
|
return JSON.stringify({ success: true, ban: banRecord });
|
|
} catch (e) {
|
|
logger.error("Failed to ban player: " + e);
|
|
throw new Error("Failed to ban player: " + e);
|
|
}
|
|
}
|
|
|
|
function rpcAdminUnbanPlayer(ctx, logger, nk, payload) {
|
|
var request = JSON.parse(payload);
|
|
|
|
requireAdmin(ctx, nk);
|
|
|
|
try {
|
|
// Get target user's account
|
|
var targetAccount = nk.accountGetId(request.user_id);
|
|
var metadata = JSON.parse(targetAccount.user.metadata || "{}");
|
|
|
|
// Remove ban
|
|
delete metadata.banned;
|
|
delete metadata.ban_reason;
|
|
delete metadata.ban_expires;
|
|
|
|
nk.accountUpdateId(request.user_id, null, null, null, null, null, null, JSON.stringify(metadata));
|
|
|
|
// Remove from ban list
|
|
nk.storageDelete([{
|
|
collection: "bans",
|
|
key: request.user_id,
|
|
userId: "00000000-0000-0000-0000-000000000000"
|
|
}]);
|
|
|
|
logger.info("Player " + request.user_id + " unbanned by " + ctx.userId);
|
|
|
|
return JSON.stringify({ success: true });
|
|
} catch (e) {
|
|
logger.error("Failed to unban player: " + e);
|
|
throw new Error("Failed to unban player");
|
|
}
|
|
}
|
|
|
|
function rpcAdminGetBanList(ctx, logger, nk, payload) {
|
|
requireAdmin(ctx, nk);
|
|
|
|
try {
|
|
var result = nk.storageList(
|
|
"00000000-0000-0000-0000-000000000000",
|
|
"bans",
|
|
100,
|
|
""
|
|
);
|
|
|
|
var bans = result.objects ? result.objects.map(function(obj) { return obj.value; }) : [];
|
|
|
|
return JSON.stringify({ bans: bans });
|
|
} catch (e) {
|
|
logger.error("Failed to get ban list: " + e);
|
|
return JSON.stringify({ bans: [] });
|
|
}
|
|
}
|
|
|
|
function rpcAdminGetServerStats(ctx, logger, nk, payload) {
|
|
var request = JSON.parse(payload || "{}");
|
|
|
|
if (request.match_id) {
|
|
requireAdminOrHost(ctx, nk, request.match_id);
|
|
} else {
|
|
requireAdmin(ctx, nk);
|
|
}
|
|
|
|
try {
|
|
// Get server-wide stats
|
|
var matches = nk.matchList(100, true, null, null, null, null);
|
|
var activeMatchCount = matches ? matches.length : 0;
|
|
|
|
var totalPlayers = 0;
|
|
if (matches) {
|
|
for (var i = 0; i < matches.length; i++) {
|
|
totalPlayers += matches[i].size || 0;
|
|
}
|
|
}
|
|
|
|
var stats = {
|
|
active_matches: activeMatchCount,
|
|
total_players: totalPlayers,
|
|
server_time: new Date().toISOString()
|
|
};
|
|
|
|
// If specific match requested, include match details
|
|
if (request.match_id) {
|
|
try {
|
|
var match = nk.matchGet(request.match_id);
|
|
if (match) {
|
|
stats.match = {
|
|
id: match.matchId,
|
|
size: match.size,
|
|
tick_rate: match.tickRate,
|
|
authoritative: match.authoritative
|
|
};
|
|
}
|
|
} catch (e) {
|
|
// Match not found
|
|
}
|
|
}
|
|
|
|
return JSON.stringify(stats);
|
|
} catch (e) {
|
|
logger.error("Failed to get server stats: " + e);
|
|
throw new Error("Failed to get server stats");
|
|
}
|
|
}
|
|
|
|
function rpcAdminGetPlayerList(ctx, logger, nk, payload) {
|
|
var request = JSON.parse(payload);
|
|
|
|
requireAdminOrHost(ctx, nk, request.match_id);
|
|
|
|
try {
|
|
var match = nk.matchGet(request.match_id);
|
|
if (!match) {
|
|
throw new Error("Match not found");
|
|
}
|
|
|
|
// Get player details
|
|
var players = [];
|
|
|
|
// Note: In actual implementation, you'd need to track presences
|
|
// This is a simplified version - adjust based on your match handler
|
|
|
|
return JSON.stringify({ players: players });
|
|
} catch (e) {
|
|
logger.error("Failed to get player list: " + e);
|
|
throw new Error("Failed to get player list");
|
|
}
|
|
}
|
|
|
|
function rpcAdminEndMatch(ctx, logger, nk, payload) {
|
|
var request = JSON.parse(payload);
|
|
|
|
requireAdminOrHost(ctx, nk, request.match_id);
|
|
|
|
try {
|
|
// Signal match to end
|
|
nk.matchSignal(request.match_id, JSON.stringify({
|
|
action: "end_match",
|
|
reason: request.reason || "Ended by admin"
|
|
}));
|
|
|
|
logger.info("Match " + request.match_id + " ended by " + ctx.userId);
|
|
|
|
return JSON.stringify({ success: true });
|
|
} catch (e) {
|
|
logger.error("Failed to end match: " + e);
|
|
throw new Error("Failed to end match");
|
|
}
|
|
}
|
|
|
|
function rpcAdminSetUserRole(ctx, logger, nk, payload) {
|
|
var request = JSON.parse(payload);
|
|
|
|
// Only owner/super-admin can set roles
|
|
var callerAccount = nk.accountGetId(ctx.userId);
|
|
var callerMetadata = JSON.parse(callerAccount.user.metadata || "{}");
|
|
|
|
if (callerMetadata.role !== "owner") {
|
|
throw new Error("Only owners can modify user roles");
|
|
}
|
|
|
|
var validRoles = ["player", "moderator", "admin"];
|
|
if (validRoles.indexOf(request.role) === -1) {
|
|
throw new Error("Invalid role");
|
|
}
|
|
|
|
try {
|
|
var targetAccount = nk.accountGetId(request.user_id);
|
|
var metadata = JSON.parse(targetAccount.user.metadata || "{}");
|
|
|
|
metadata.role = request.role;
|
|
|
|
nk.accountUpdateId(request.user_id, null, null, null, null, null, null, JSON.stringify(metadata));
|
|
|
|
logger.info("User " + request.user_id + " role set to " + request.role + " by " + ctx.userId);
|
|
|
|
return JSON.stringify({ success: true, role: request.role });
|
|
} catch (e) {
|
|
logger.error("Failed to set user role: " + e);
|
|
throw new Error("Failed to set user role");
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// User Profile RPCs
|
|
// =============================================================================
|
|
|
|
function rpcGetUserProfile(ctx, logger, nk, payload) {
|
|
var request = JSON.parse(payload || "{}");
|
|
var targetUserId = request.user_id || ctx.userId;
|
|
|
|
try {
|
|
var account = nk.accountGetId(targetUserId);
|
|
var metadata = JSON.parse(account.user.metadata || "{}");
|
|
|
|
// Check if banned
|
|
if (metadata.banned && targetUserId === ctx.userId) {
|
|
// Check if ban expired
|
|
if (metadata.ban_expires) {
|
|
var expiresAt = new Date(metadata.ban_expires);
|
|
if (expiresAt <= new Date()) {
|
|
// Ban expired, remove it
|
|
delete metadata.banned;
|
|
delete metadata.ban_reason;
|
|
delete metadata.ban_expires;
|
|
nk.accountUpdateId(targetUserId, null, null, null, null, null, null, JSON.stringify(metadata));
|
|
} else {
|
|
throw new Error("Account banned until " + metadata.ban_expires + ". Reason: " + metadata.ban_reason);
|
|
}
|
|
} else {
|
|
throw new Error("Account permanently banned. Reason: " + metadata.ban_reason);
|
|
}
|
|
}
|
|
|
|
return JSON.stringify({
|
|
user_id: account.user.id,
|
|
username: account.user.username,
|
|
display_name: account.user.displayName,
|
|
avatar_url: account.user.avatarUrl,
|
|
create_time: account.user.createTime,
|
|
role: metadata.role || "player"
|
|
});
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
function rpcUpdateUserProfile(ctx, logger, nk, payload) {
|
|
if (!ctx.userId) {
|
|
throw new Error("Not authenticated");
|
|
}
|
|
|
|
var request = JSON.parse(payload);
|
|
|
|
try {
|
|
nk.accountUpdateId(
|
|
ctx.userId,
|
|
null, // username
|
|
request.display_name || null,
|
|
null, // timezone
|
|
null, // location
|
|
null, // lang
|
|
request.avatar_url || null,
|
|
null // metadata
|
|
);
|
|
|
|
return JSON.stringify({ success: true });
|
|
} catch (e) {
|
|
logger.error("Failed to update profile: " + e);
|
|
throw new Error("Failed to update profile");
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Leaderboard RPCs
|
|
// =============================================================================
|
|
|
|
function rpcGetLeaderboardStats(ctx, logger, nk, payload) {
|
|
try {
|
|
var limit = 50;
|
|
var records = nk.leaderboardRecordsList("global_high_score", null, limit, "");
|
|
var leaderboardData = [];
|
|
|
|
var ownerRecords = records.records || [];
|
|
for (var i = 0; i < ownerRecords.length; i++) {
|
|
var record = ownerRecords[i];
|
|
var metadata = {};
|
|
try { metadata = JSON.parse(record.metadata || "{}"); } catch (e) {}
|
|
|
|
leaderboardData.push({
|
|
user_id: record.ownerId,
|
|
username: record.username,
|
|
display_name: record.displayName || record.username,
|
|
avatar_url: metadata.avatar_url || "",
|
|
high_score: record.score || 0,
|
|
games_played: metadata.games_played || 0,
|
|
games_won: metadata.games_won || 0
|
|
});
|
|
}
|
|
|
|
return JSON.stringify({ leaderboard: leaderboardData });
|
|
} catch (e) {
|
|
logger.error("Failed to get native leaderboard stats: " + e);
|
|
// Fallback to storage-based if native fails (e.g. not created yet)
|
|
return JSON.stringify({ leaderboard: [] });
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Admin User Management RPCs
|
|
// =============================================================================
|
|
|
|
function rpcAdminListUsers(ctx, logger, nk, payload) {
|
|
requireAdmin(ctx, nk);
|
|
|
|
try {
|
|
var users = [];
|
|
var rows = nk.sqlQuery("SELECT id, username, display_name, metadata, create_time FROM users WHERE id != '00000000-0000-0000-0000-000000000000' ORDER BY create_time DESC LIMIT 500", []);
|
|
|
|
for (var i = 0; i < rows.length; i++) {
|
|
var row = rows[i];
|
|
var metadata = {};
|
|
try { metadata = JSON.parse(row.metadata || "{}"); } catch(e) {}
|
|
users.push({
|
|
user_id: row.id,
|
|
username: row.username || "",
|
|
display_name: row.display_name || row.username || "",
|
|
create_time: row.create_time,
|
|
role: metadata.role || "player",
|
|
banned: metadata.banned || false,
|
|
ban_reason: metadata.ban_reason || ""
|
|
});
|
|
}
|
|
|
|
return JSON.stringify({ users: users, count: users.length });
|
|
} catch (e) {
|
|
logger.error("Failed to list users: " + e);
|
|
throw new Error("Failed to list users: " + e);
|
|
}
|
|
}
|
|
|
|
function rpcAdminDeleteUsers(ctx, logger, nk, payload) {
|
|
requireAdmin(ctx, nk);
|
|
|
|
var request = JSON.parse(payload);
|
|
var userIds = request.user_ids || [];
|
|
|
|
if (userIds.length === 0) {
|
|
throw new Error("No user IDs provided");
|
|
}
|
|
|
|
// Don't allow deleting yourself
|
|
for (var i = 0; i < userIds.length; i++) {
|
|
if (userIds[i] === ctx.userId) {
|
|
throw new Error("Cannot delete your own account");
|
|
}
|
|
}
|
|
|
|
var deleted = [];
|
|
var failed = [];
|
|
|
|
for (var j = 0; j < userIds.length; j++) {
|
|
var uid = userIds[j];
|
|
try {
|
|
// Check if target is admin — don't allow deleting admins
|
|
var account = nk.accountGetId(uid);
|
|
var meta = {};
|
|
try { meta = JSON.parse(account.user.metadata || "{}"); } catch(e) {}
|
|
if (ADMIN_ROLES.indexOf(meta.role || "") !== -1) {
|
|
failed.push({ user_id: uid, reason: "Cannot delete admin account" });
|
|
continue;
|
|
}
|
|
nk.accountDeleteId(uid, false);
|
|
deleted.push(uid);
|
|
logger.warn("User " + uid + " deleted by " + ctx.userId);
|
|
} catch (e) {
|
|
failed.push({ user_id: uid, reason: "" + e });
|
|
}
|
|
}
|
|
|
|
return JSON.stringify({ success: true, deleted: deleted, failed: failed });
|
|
}
|
|
|
|
function rpcAdminUpdateStats(ctx, logger, nk, payload) {
|
|
requireAdmin(ctx, nk);
|
|
|
|
var request = JSON.parse(payload);
|
|
var targetUserId = request.user_id;
|
|
var stats = request.stats; // { high_score: X, games_played: Y, games_won: Z }
|
|
|
|
if (!targetUserId || !stats) {
|
|
throw new Error("User ID and stats are required");
|
|
}
|
|
|
|
try {
|
|
// 1. Update Storage (for legacy support/redundancy)
|
|
nk.storageWrite([{
|
|
collection: "stats",
|
|
key: "stats",
|
|
userId: targetUserId,
|
|
value: JSON.stringify(stats),
|
|
permissionRead: 1, // Public read
|
|
permissionWrite: 0 // No one can write (except server)
|
|
}]);
|
|
|
|
// 2. Update Native Leaderboard
|
|
var account = nk.accountGetId(targetUserId);
|
|
var score = stats.high_score || 0;
|
|
var subscore = 0;
|
|
var metadata = {
|
|
games_played: stats.games_played || 0,
|
|
games_won: stats.games_won || 0,
|
|
avatar_url: account.user.avatarUrl || ""
|
|
};
|
|
|
|
nk.leaderboardRecordWrite("global_high_score", targetUserId, account.user.username, score, subscore, JSON.stringify(metadata));
|
|
|
|
logger.info("Stats updated for user " + targetUserId + " by admin " + ctx.userId + " (Storage + Native)");
|
|
return JSON.stringify({ success: true });
|
|
} catch (e) {
|
|
logger.error("Failed to update stats: " + e);
|
|
throw new Error("Failed to update stats: " + e);
|
|
}
|
|
}
|
|
|
|
function rpcAdminDeleteStats(ctx, logger, nk, payload) {
|
|
requireAdmin(ctx, nk);
|
|
|
|
var request = JSON.parse(payload);
|
|
var targetUserId = request.user_id;
|
|
|
|
if (!targetUserId) {
|
|
throw new Error("User ID is required");
|
|
}
|
|
|
|
try {
|
|
// 1. Delete Storage
|
|
nk.storageDelete([{
|
|
collection: "stats",
|
|
key: "stats",
|
|
userId: targetUserId
|
|
}]);
|
|
|
|
// 2. Delete Native Leaderboard Record
|
|
nk.leaderboardRecordDelete("global_high_score", targetUserId);
|
|
|
|
logger.info("Stats deleted for user " + targetUserId + " by admin " + ctx.userId);
|
|
return JSON.stringify({ success: true });
|
|
} catch (e) {
|
|
logger.error("Failed to delete stats: " + e);
|
|
throw new Error("Failed to delete stats");
|
|
}
|
|
}
|
|
|
|
function rpcAdminSyncLeaderboard(ctx, logger, nk, payload) {
|
|
requireAdmin(ctx, nk);
|
|
|
|
try {
|
|
var result = nk.storageList(null, "stats", 100, "");
|
|
var statsObjects = result.objects || [];
|
|
var count = 0;
|
|
|
|
for (var i = 0; i < statsObjects.length; i++) {
|
|
var obj = statsObjects[i];
|
|
var userId = obj.userId;
|
|
var stats = JSON.parse(obj.value);
|
|
|
|
try {
|
|
var account = nk.accountGetId(userId);
|
|
var metadata = {
|
|
games_played: stats.games_played || 0,
|
|
games_won: stats.games_won || 0,
|
|
avatar_url: account.user.avatarUrl || ""
|
|
};
|
|
|
|
nk.leaderboardRecordWrite("global_high_score", userId, account.user.username, stats.high_score || 0, 0, JSON.stringify(metadata));
|
|
count++;
|
|
} catch (inner) {
|
|
logger.error("Failed to sync record for " + userId + ": " + inner);
|
|
}
|
|
}
|
|
|
|
return JSON.stringify({ success: true, synced: count });
|
|
} catch (e) {
|
|
logger.error("Leaderboard sync failed: " + e);
|
|
throw new Error("Sync failed: " + e);
|
|
}
|
|
}
|
|
|
|
// Before login hook to check ban status
|
|
function beforeAuthenticateEmail(ctx, logger, nk, data) {
|
|
// Can't check ban before auth, so we check in afterAuthenticate
|
|
return data;
|
|
}
|