Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ban category api #18

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions RPMTW-Server.iml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>
1 change: 1 addition & 0 deletions lib/database/models/auth/ban_category.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enum BanCategory { universeChat, permanent }
79 changes: 59 additions & 20 deletions lib/database/models/auth/ban_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,59 @@ import 'package:rpmtw_server/database/database.dart';

import 'package:rpmtw_server/database/db_model.dart';
import 'package:rpmtw_server/database/index_fields.dart';
import 'package:rpmtw_server/database/model_field.dart';
import 'package:rpmtw_server/database/models/auth/ban_category.dart';

class BanInfo extends DBModel {
static const String collectionName = 'ban_infos';
static const List<IndexField> indexFields = [
IndexField('ip', unique: true),
];

/// 被封鎖的 IP
final String ip;
/// The IP address of the banned user
final String? ip;

/// 封鎖原因
final String reason;

/// 使用此 IP 登入的使用者帳號 UUID
final BanCategory category;

final List<String> userUUID;

final DateTime createdAt;

final String? operatorUUID;

const BanInfo({
required this.ip,
this.ip,
required this.reason,
required this.category,
required this.userUUID,
required this.createdAt,
this.operatorUUID,
required String uuid,
}) : super(uuid: uuid);

BanInfo copyWith({
String? ip,
String? reason,
List<String>? userUUID,
}) {
return BanInfo(
ip: ip ?? this.ip,
reason: reason ?? this.reason,
userUUID: userUUID ?? this.userUUID,
uuid: uuid,
);
}

@override
Map<String, dynamic> toMap() {
return {
'ip': ip,
'reason': reason,
'category': category.name,
'userUUID': userUUID,
'createdAt': createdAt.millisecondsSinceEpoch,
'operatorUUID': operatorUUID,
'uuid': uuid,
};
}

@override
Map<String, dynamic> outputMap() {
return {
'reason': reason,
'category': category.name,
'userUUID': userUUID,
'createdAt': createdAt.millisecondsSinceEpoch,
'operatorUUID': operatorUUID,
'uuid': uuid,
};
}
Expand All @@ -52,11 +63,39 @@ class BanInfo extends DBModel {
return BanInfo(
ip: map['ip'],
reason: map['reason'],
category: BanCategory.values.byName(map['category']),
userUUID: List<String>.from(map['userUUID']),
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']),
operatorUUID: map['operatorUUID'],
uuid: map['uuid']!,
);
}

static Future<BanInfo?> getByIP(String ip) async =>
DataBase.instance.getModelByField<BanInfo>('ip', ip);
static Future<BanInfo?> getByUUID(String uuid) async =>
DataBase.instance.getModelByUUID<BanInfo>(uuid);

static Future<List<BanInfo>> getByUserUUID(String userUUID) async =>
DataBase.instance
.getModelsByField<BanInfo>([ModelField('userUUID', userUUID)]);

static Future<List<BanInfo>> getByIP(String ip) async =>
DataBase.instance.getModelsByField<BanInfo>([ModelField('ip', ip)]);

static Future<BanInfo?> isBanned(BanCategory category,
{String? ip, String? userUUID}) async {
List<BanInfo> bans = [];
if (ip != null) {
bans.addAll(await getByIP(ip));
}
if (userUUID != null) {
bans.addAll(await getByUserUUID(userUUID));
}

try {
return bans.firstWhere((ban) =>
ban.category == category || ban.category == BanCategory.permanent);
} catch (e) {
return null;
}
}
}
20 changes: 16 additions & 4 deletions lib/handler/auth_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:mailer/mailer.dart';
import 'package:mailer/smtp_server.dart';
import 'package:mongo_dart/mongo_dart.dart';
import 'package:rpmtw_server/database/models/auth/auth_code_.dart';
import 'package:rpmtw_server/database/models/auth/ban_category.dart';
import 'package:rpmtw_server/database/models/auth/ban_info.dart';
import 'package:rpmtw_server/utilities/api_response.dart';
import 'package:shelf/shelf.dart';
Expand All @@ -24,6 +25,13 @@ class AuthHandler {
return jwt.sign(AuthHandler.secretKey);
}

static String generatePasswordHash(String password) {
final dbCrypt = DBCrypt();
// Generate salt, 10 rounds by default
final String salt = dbCrypt.gensaltWithRounds(AuthHandler.saltRounds);
return dbCrypt.hashpw(password, salt); // Hash the password with the salt
}

static Future<AuthCode> generateAuthCode(
String email, String userUUID) async {
AuthCode authCode = AuthCode.create(email);
Expand All @@ -35,10 +43,13 @@ class AuthHandler {
return (request) {
return Future.sync(() async {
try {
BanInfo? banInfo = await BanInfo.getByIP(request.ip);
if (banInfo != null) {
// 檢查是否被封鎖
return APIResponse.banned(reason: banInfo.reason);
// Check the user is banned or not
BanInfo? info =
await BanInfo.isBanned(BanCategory.permanent, ip: request.ip);

if (info != null) {
return APIResponse.banned(
reason: info.reason, category: info.category);
}
} catch (e) {
return APIResponse.internalServerError();
Expand Down Expand Up @@ -243,6 +254,7 @@ abstract class _BaseValidatedResult {

/// 驗證結果訊息
String message;

_BaseValidatedResult(this.isValid, this.code, this.message);

Map toMap() {
Expand Down
11 changes: 10 additions & 1 deletion lib/handler/universe_chat_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:dotenv/dotenv.dart';
import 'package:http/http.dart' as http;
import 'package:mongo_dart/mongo_dart.dart';
import 'package:rpmtw_dart_common_library/rpmtw_dart_common_library.dart';
import 'package:rpmtw_server/database/models/auth/ban_category.dart';
import 'package:rpmtw_server/database/models/auth/user_role.dart';
import 'package:rpmtw_server/utilities/request_extension.dart';
import 'package:socket_io/socket_io.dart';
Expand Down Expand Up @@ -127,7 +128,14 @@ class UniverseChatHandler {

BanInfo? banInfo;
fetch() async {
banInfo = await BanInfo.getByIP(ip.address);
List<BanInfo> banInfos = await BanInfo.getByIP(ip.address);
for (final info in banInfos) {
if (info.category == BanCategory.universeChat ||
info.category == BanCategory.permanent) {
banInfo = info;
break;
}
}
initCheckList[1] = true;
}

Expand Down Expand Up @@ -304,6 +312,7 @@ class UniverseChatHandler {
class _CacheMinecraftInfo {
final String uuid;
final String name;

const _CacheMinecraftInfo({
required this.uuid,
required this.name,
Expand Down
1 change: 1 addition & 0 deletions lib/routes/api_route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:shelf_router/shelf_router.dart';

abstract class APIRoute {
String get routeName;

void router(Router router) => throw UnimplementedError();

void register(Router mainRouter) {
Expand Down
51 changes: 45 additions & 6 deletions lib/routes/auth_route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import 'package:dbcrypt/dbcrypt.dart';
import 'package:mongo_dart/mongo_dart.dart';
import 'package:rpmtw_server/database/database.dart';
import 'package:rpmtw_server/database/models/auth/auth_code_.dart';
import 'package:rpmtw_server/database/models/auth/ban_category.dart';
import 'package:rpmtw_server/database/models/auth/ban_info.dart';
import 'package:rpmtw_server/database/models/auth/user.dart';
import 'package:rpmtw_server/database/auth_route.dart';
import 'package:rpmtw_server/database/models/auth/user_role.dart';
import 'package:rpmtw_server/database/models/storage/storage.dart';
import 'package:rpmtw_server/handler/auth_handler.dart';
import 'package:rpmtw_server/utilities/api_response.dart';
Expand All @@ -29,17 +32,13 @@ class AuthRoute extends APIRoute {
if (!emailValidatedResult.isValid) {
return APIResponse.badRequest(message: emailValidatedResult.message);
}
DBCrypt dbCrypt = DBCrypt();
String salt =
dbCrypt.gensaltWithRounds(AuthHandler.saltRounds); // 生成鹽,加密次數為10次
String hash = dbCrypt.hashpw(password, salt); //使用加鹽算法將明文密碼生成為雜湊值

User user = User(
username: data.fields['username'],
email: email,
avatarStorageUUID: data.fields['avatarStorageUUID'],
emailVerified: false,
passwordHash: hash,
passwordHash: AuthHandler.generatePasswordHash(password),
uuid: Uuid().v4(),
loginIPs: [req.ip]);

Expand Down Expand Up @@ -179,7 +178,7 @@ class AuthRoute extends APIRoute {
{
'uuid': 'e5634ad4-529d-42d4-9a56-045c5f5888cd',
'password': 'test'
}
}
*/
router.postRoute('/get-token', (req, data) async {
String uuid = data.fields['uuid'];
Expand Down Expand Up @@ -212,5 +211,45 @@ class AuthRoute extends APIRoute {
'isValid': isValid,
});
}, requiredFields: ['authCode', 'email']);

// Get ban info
router.getRoute('/ban/<uuid>', (req, data) async {
final String uuid = data.fields['uuid']!;
BanInfo? ban = await BanInfo.getByUUID(uuid);
if (ban == null) {
return APIResponse.modelNotFound<BanInfo>();
}

return APIResponse.success(data: ban.outputMap());
}, authConfig: AuthConfig(role: UserRoleType.admin));
// Ban a user by ip or user uuid
router.postRoute('/ban', (req, data) async {
final operator = req.user!;
final banned = await User.getByUUID(data.fields['banned']!);

if (banned == null) {
return APIResponse.modelNotFound<User>();
}

final String reason = data.fields['reason']!;
final category = BanCategory.values.byName(data.fields['category']!);

// final info = BanInfo(
// ip: ip,
// reason: reason,
// category: category,
// userUUID: [banned.uuid],
// createdAt: DateTime.now(),
// operatorUUID: operator.uuid,
// uuid: Uuid().v4());

// await info.insert();

// return APIResponse.success(data: info.outputMap());
}, authConfig: AuthConfig());
// Search the ban infos
router.getRoute('/bans', (req, data) async {
bool canAccess;
});
}
}
6 changes: 4 additions & 2 deletions lib/utilities/api_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:convert';

import 'package:rpmtw_dart_common_library/rpmtw_dart_common_library.dart';
import 'package:rpmtw_server/database/list_model_response.dart';
import 'package:rpmtw_server/database/models/auth/ban_category.dart';
import 'package:rpmtw_server/utilities/messages.dart';
import 'package:shelf/shelf.dart';
import 'dart:io';
Expand Down Expand Up @@ -84,12 +85,13 @@ class APIResponse {
static Response modelNotFound<T>({String? modelName}) => notFound(
'${modelName ?? T.toString().toCapitalizedWithSpace()} not found');

static Response banned({required String reason}) =>
static Response banned(
{required String reason, required BanCategory category}) =>
Response(HttpStatus.forbidden,
body: json.encode({
'status': HttpStatus.forbidden,
'message': 'Banned',
'data': {'reason': reason}
'data': {'reason': reason, 'category': category.name}
}),
headers: _baseHeaders);
}
21 changes: 13 additions & 8 deletions lib/utilities/request_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'dart:typed_data';

import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:http/http.dart' as http;
import 'package:rpmtw_server/database/models/auth/ban_category.dart';
import 'package:rpmtw_server/database/models/auth/ban_info.dart';
import 'package:rpmtw_server/database/models/auth/user.dart';
import 'package:rpmtw_server/database/auth_route.dart';
import 'package:rpmtw_server/utilities/api_response.dart';
Expand All @@ -14,15 +16,10 @@ import 'package:shelf_router/shelf_router.dart';

extension RequestExtension on Request {
String get ip {
String? xForwardedFor = headers['X-Forwarded-For'];
if (xForwardedFor != null && kTestMode) {
return xForwardedFor;
String? cfIP = headers['CF-Connecting-IP'];
if (cfIP != null) {
return cfIP;
} else {
String? cfIP = headers['CF-Connecting-IP'];
if (cfIP != null) {
return cfIP;
}

HttpConnectionInfo connectionInfo =
context['shelf.io.connection_info'] as HttpConnectionInfo;
InternetAddress internetAddress = connectionInfo.remoteAddress;
Expand Down Expand Up @@ -91,6 +88,13 @@ extension RouterExtension on Router {
await _newUser.update();
}

BanInfo? info = await BanInfo.isBanned(BanCategory.permanent,
ip: clientIP, userUUID: user.uuid);
if (info != null) {
return APIResponse.banned(
reason: info.reason, category: info.category);
}

request = request
.change(context: {'user': user, 'isAuthenticated': true});

Expand Down Expand Up @@ -184,6 +188,7 @@ class RouteData {
final Uint8List bytes;

Stream<List<int>> get byteStream => http.ByteStream.fromBytes(bytes);

String get body => utf8.decode(bytes);

RouteData(this.fields, this.bytes);
Expand Down
Loading