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/organizations #8

Merged
merged 6 commits into from
Apr 8, 2022
Merged
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
22 changes: 16 additions & 6 deletions lib/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,21 @@ import 'package:dotenv/dotenv.dart';
abstract class Constants {
static const usersCollection = 'users';
static const refreshTokensCollection = 'refreshTokens';
static final jwtRefreshSignature = (Platform.environment['JWT_REFRESH_TOKEN_SIGNATURE'] ?? env['JWT_REFRESH_TOKEN_SIGNATURE'])!;
static final jwtAccessSignature = (Platform.environment['JWT_ACCESS_TOKEN_SIGNATURE'] ?? env['JWT_ACCESS_TOKEN_SIGNATURE'])!;
static final mongoConnectionString = (Platform.environment['MONGO_CONNECTION'] ?? env['MONGO_CONNECTION'])!;
static final imgurClientId = (Platform.environment['IMGUR_CLIENT_ID'] ?? env['IMGUR_CLIENT_ID'])!;
static const organizationsCollection = 'organization';
static final jwtRefreshSignature =
(Platform.environment['JWT_REFRESH_TOKEN_SIGNATURE'] ??
env['JWT_REFRESH_TOKEN_SIGNATURE'])!;
static final jwtAccessSignature =
(Platform.environment['JWT_ACCESS_TOKEN_SIGNATURE'] ??
env['JWT_ACCESS_TOKEN_SIGNATURE'])!;
static final mongoConnectionString =
(Platform.environment['MONGO_CONNECTION'] ?? env['MONGO_CONNECTION'])!;
static final imgurClientId =
(Platform.environment['IMGUR_CLIENT_ID'] ?? env['IMGUR_CLIENT_ID'])!;

static final passwordRegExp = RegExp(r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!@#\$&*~]).{8,}$');
static final emailRegExp = RegExp(r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?");
static final passwordRegExp =
RegExp(r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[!@#\$&*~]).{8,}$');
static final emailRegExp = RegExp(
r"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?",
);
}
1 change: 1 addition & 0 deletions lib/src/database/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class DatabaseService {

DbCollection get usersCollection => _db.collection('users');
DbCollection get refreshTokensCollection => _db.collection('refreshTokens');
DbCollection get organizationsCollection => _db.collection('organizations');

Future open() => _db.open();
Future close() => _db.close();
Expand Down
4 changes: 1 addition & 3 deletions lib/src/db_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:mongo_dart/mongo_dart.dart';
abstract class DBModel<T> {
DBModel(this.collection, {this.id});

@JsonKey(name: '_id', includeIfNull: false)
@JsonKey(name: '_id', includeIfNull: false, fromJson: ObjectId.parse)
ObjectId? id;

@JsonKey(ignore: true)
Expand All @@ -15,9 +15,7 @@ abstract class DBModel<T> {
final result = await collection.modernUpdate(
where.eq('_id', id),
toJson(),
upsert: true,
);

return result;
} else {
final payload = toJson();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:alfred/alfred.dart';
import 'package:backend/src/organization/models/organization.dart';
import 'package:backend/src/services/services.dart';
import 'package:backend/src/user/models/user.dart';
import 'package:backend/src/validators/auth_validator.dart';
import 'package:mongo_dart/mongo_dart.dart';

part 'create_organization_controller.dart';
part 'create_organization_middleware.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
part of 'create_organization.dart';

class CreateOrganizationController {
const CreateOrganizationController();

Future<dynamic> call(HttpRequest req, HttpResponse res) async {
final name = req.store.get<String>('name');
final homePageUrl = req.store.tryGet<String>('homePageUrl');
final user = req.store.get<User>('user');

final organization = Organization(
name: name,
admin: user.id!.$oid,
homePageUrl: homePageUrl,
);

try {
final result = await services.organizations.addToDatabase(
organization,
);

user.organizations ??= [];
user.organizations?.add(result.id as ObjectId);

await user.save();

res.statusCode = 200;
await res.json(
<String, dynamic>{
'id': result.id,
...organization.toJson(),
},
);
} catch (e) {
throw AlfredException(500, {
'message': 'an unknown error occurred',
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
part of 'create_organization.dart';

class CreateOrganizationMiddleware extends AuthenticationMiddleware {
const CreateOrganizationMiddleware();

@override
Future<dynamic> call(HttpRequest req, HttpResponse res) async {
await super.call(req, res);
final body = await req.bodyAsJsonMap;

final dynamic name = body['name'];
final dynamic homePageUrl = body['homePageUrl'];

if (name == null || (name as String).isEmpty) {
res.reasonPhrase = 'nameRequired';
throw AlfredException(400, {
'message': 'name is required!',
});
} else if (name.length > 30) {
res.reasonPhrase = 'nameTooLong';
throw AlfredException(400, {
'message': 'name is too long. max length is 30!',
});
}

final userId = req.store.get<User>('user').id;
if (userId == null) {
res.reasonPhrase = 'userIdNotFound';
throw AlfredException(500, {
'message': 'userId is null',
});
}

final organizationExists = await services.organizations
.findOrganizationByNameAndUserId(name: name, userId: userId.$oid) !=
null;

if (organizationExists) {
res.reasonPhrase = 'organizationAlreadyExists';
throw AlfredException(409, {
'message': 'you already created an organization with that name!',
});
}

req.store.set('name', name);
req.store.set('homePageUrl', homePageUrl);
}
}
35 changes: 35 additions & 0 deletions lib/src/organization/models/organization.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:backend/src/database/database.dart';
import 'package:backend/src/db_model.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:mongo_dart/mongo_dart.dart';

part 'organization.g.dart';

@JsonSerializable(explicitToJson: true)
class Organization extends DBModel<Organization> {
Organization({
required this.name,
required this.admin,
this.homePageUrl,
this.imageUrl,
this.employers,
this.employees,
}) : super(database.usersCollection);

factory Organization.fromJson(Map<String, dynamic> json) =>
_$OrganizationFromJson(json);

String name;
String admin;
String? homePageUrl;
String? imageUrl;
List<ObjectId>? employers;
List<ObjectId>? employees;

@override
Map<String, dynamic> toJson() => _$OrganizationToJson(this);

@override
Organization fromJson(Map<String, dynamic> json) =>
Organization.fromJson(json);
}
37 changes: 37 additions & 0 deletions lib/src/organization/models/organization.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions lib/src/organization/organization_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'package:backend/src/database/database.dart';
import 'package:backend/src/organization/models/organization.dart';
import 'package:mongo_dart/mongo_dart.dart';

class OrganizationService {
OrganizationService(this.dbService);

final DatabaseService dbService;

Future<Organization?> findOrganizationById({
required String id,
}) async {
final organization =
await dbService.usersCollection.findOne(where.id(ObjectId.parse(id)));

if (organization == null || organization.isEmpty) {
return null;
}

return Organization.fromJson(organization);
}

Future<WriteResult> addToDatabase(Organization organization) async {
return dbService.organizationsCollection.insertOne(
organization.toJson(),
);
}

Future<Organization?> findOrganizationByNameAndUserId({
required String name,
required String userId,
}) async {
final organization = await dbService.organizationsCollection
.findOne(where.eq('name', name).eq('admin', userId));

if (organization == null || organization.isEmpty) {
return null;
}

return Organization.fromJson(organization);
}
}
11 changes: 10 additions & 1 deletion lib/src/server.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:alfred/alfred.dart';
import 'package:backend/src/organization/create_organization/create_organization.dart';
import 'package:backend/src/user/current/current.dart';
import 'package:backend/src/user/user.dart';
import 'package:backend/src/validators/auth_validator.dart';
Expand All @@ -11,7 +12,10 @@ class Server {
Future<void> init() async {
// initialize alfred:
final app = Alfred(
onNotFound: (req, res) => throw AlfredException(404, {'message': '${req.requestedUri.path} not found'}),
onNotFound: (req, res) => throw AlfredException(
404,
{'message': '${req.requestedUri.path} not found'},
),
onInternalError: errorHandler,
)
..post(
Expand All @@ -38,6 +42,11 @@ class Server {
const AuthenticationMiddleware(),
],
)
..post(
'organization/create',
const CreateOrganizationController(),
middleware: [const CreateOrganizationMiddleware()],
)
..delete(
'user/logout',
const UserLogoutController(),
Expand Down
2 changes: 2 additions & 0 deletions lib/src/services/services.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:backend/backend.dart';
import 'package:backend/src/database/database.dart';
import 'package:backend/src/imgur/imgur_client.dart';
import 'package:backend/src/organization/organization_service.dart';
import 'package:backend/src/user/tokens_service.dart';
import 'package:backend/src/user/user_service.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
Expand All @@ -13,6 +14,7 @@ class Services {

late final users = UsersService(dbService);
late final tokens = TokensService(dbService);
late final organizations = OrganizationService(dbService);

late final imgurClient = ImgurClient(Constants.imgurClientId);

Expand Down
2 changes: 2 additions & 0 deletions lib/src/user/models/user.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class User extends DBModel<User> {
this.country,
this.city,
this.photo,
this.organizations,
}) : super(database.usersCollection);

factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Expand All @@ -26,6 +27,7 @@ class User extends DBModel<User> {
String? country;
String? city;
String? photo;
List<ObjectId>? organizations;

@override
Map<String, dynamic> toJson({bool showPassword = true}) => _$UserToJson(this, showPassword);
Expand Down
4 changes: 3 additions & 1 deletion lib/src/user/models/user.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.