import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; import 'package:argon2/argon2.dart'; import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:http/http.dart' as http; import 'package:postgres/postgres.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as io; import 'package:shelf_router/shelf_router.dart'; SecretKey secretKey = SecretKey(Platform.environment['JOBLINK_SECRET_KEY']!); enum BusinessType { food, shop, outdoors, manufacturing, entertainment, other, } class Business { int id; String name; String description; BusinessType type; String website; String contactName; String contactEmail; String contactPhone; String notes; String locationName; String locationAddress; Business({ required this.id, required this.name, required this.description, required this.type, required this.website, required this.contactName, required this.contactEmail, required this.contactPhone, required this.notes, required this.locationName, required this.locationAddress, }); factory Business.fromJson(Map json) { bool typeValid = true; try { BusinessType.values.byName(json['type']); } catch (e) { typeValid = false; } return Business( id: json['id'], name: json['name'], description: json['description'], type: typeValid ? BusinessType.values.byName(json['type']) : BusinessType.other, website: json['website'], contactName: json['contactName'], contactEmail: json['contactEmail'], contactPhone: json['contactPhone'], notes: json['notes'], locationName: json['locationName'], locationAddress: json['locationAddress'], ); } } Future fetchBusinessData() async { final result = await postgres.query(''' SELECT json_agg( json_build_object( 'id', id, 'name', name, 'description', description, 'type', type, 'website', website, 'contactName', "contactName", 'contactEmail', "contactEmail", 'contactPhone', "contactPhone", 'notes', notes, 'locationName', "locationName", 'locationAddress', "locationAddress" ) ) FROM businesses '''); var encoded = json.encode(result); var decoded = json.decode(encoded); encoded = json.encode(decoded[0][0]); return encoded; } //set defaults String _hostname = 'localhost'; const _port = 8000; final postgres = PostgreSQLConnection( Platform.environment['JOBLINK_POSTGRES_ADDRESS']!, int.parse(Platform.environment['JOBLINK_POSTGRES_PORT']!), 'fbla', username: Platform.environment['JOBLINK_POSTGRES_USERNAME'], password: Platform.environment['JOBLINK_POSTGRES_PASSWORD'], ); void main() async { await postgres.open(); final app = Router(); // routes app.get('/fbla-api/hello', (Request request) async { print('Hello received'); return Response.ok( 'Hello, World!', headers: {'Access-Control-Allow-Origin': '*'}, ); }); app.get('/fbla-api/businessdata', (Request request) async { print('business data request received'); final output = await fetchBusinessData(); return Response.ok( output.toString(), headers: {'Access-Control-Allow-Origin': '*'}, ); }); app.get('/fbla-api/logos/', (Request request, String logoId) { print('business logo request received'); var logo = File('logos/$logoId.png'); List content = logo.readAsBytesSync(); return Response.ok( content, headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'image/png' }, ); }); app.post('/fbla-api/createbusiness', (Request request) async { print('create business request received'); final payload = await request.readAsString(); var auth = request.headers['Authorization']?.replaceAll('Bearer ', ''); try { JWT.verify(auth!, secretKey); var json = jsonDecode(payload); Business business = Business.fromJson(json); await postgres.query(''' INSERT INTO businesses (name, description, type, website, "contactName", "contactPhone", "contactEmail", notes, "locationName", "locationAddress") VALUES ('${business.name.replaceAll("'", "''")}', '${business.description.replaceAll("'", "''")}', '${business.type.name}', '${business.website}', '${business.contactName.replaceAll("'", "''")}', '${business.contactPhone}', '${business.contactEmail}', '${business.notes.replaceAll("'", "''")}', '${business.locationName.replaceAll("'", "''")}', '${business.locationAddress.replaceAll("'", "''")}') '''); final dbBusiness = await postgres.query('''SELECT * FROM public.businesses ORDER BY id DESC LIMIT 1'''); var id = dbBusiness[0][0]; var logoResponse = await http.get( Uri.http('logo.clearbit.com', '/${business.website}'), ); if (logoResponse.headers.toString().contains('image/png')) { await File('logos/$id.png').writeAsBytes(logoResponse.bodyBytes); } return Response.ok( id.toString(), headers: {'Access-Control-Allow-Origin': '*'}, ); } on JWTExpiredException { print('JWT Expired'); } on JWTException catch (e) { print(e.message); } return Response.unauthorized( 'unauthorized', headers: {'Access-Control-Allow-Origin': '*'}, ); }); app.post('/fbla-api/deletebusiness', (Request request) async { print('delete business request received'); final payload = await request.readAsString(); var auth = request.headers['Authorization']?.replaceAll('Bearer ', ''); try { JWT.verify(auth!, secretKey); var json = jsonDecode(payload); var id = json['id']; await postgres.query(''' DELETE FROM public.businesses WHERE id IN ($id); '''); try { await File('logos/$id.png').delete(); } catch (e) { print('Failure to delete logo! $e'); } return Response.ok( id.toString(), headers: {'Access-Control-Allow-Origin': '*'}, ); } on JWTExpiredException { print('JWT Expired'); } on JWTException catch (e) { print(e.message); } return Response.unauthorized( 'unauthorized', headers: {'Access-Control-Allow-Origin': '*'}, ); }); app.post('/fbla-api/editbusiness', (Request request) async { print('edit business request received'); final payload = await request.readAsString(); var auth = request.headers['Authorization']?.replaceAll('Bearer ', ''); try { JWT.verify(auth!, secretKey); var json = jsonDecode(payload); Business business = Business.fromJson(json); await postgres.query(''' UPDATE businesses SET name = '${business.name.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, description = '${business.description.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, website = '${business.website}'::text, type = '${business.type.name}'::text, "contactName" = '${business.contactName.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "contactPhone" = '${business.contactPhone}'::text, "contactEmail" = '${business.contactEmail}'::text, notes = '${business.notes.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "locationName" = '${business.locationName.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text, "locationAddress" = '${business.locationAddress.replaceAll("'", "''").replaceAll("\"", "\"\"")}'::text WHERE id = ${business.id}; '''); var logoResponse = await http.get( Uri.http('logo.clearbit.com', '/${business.website}'), ); try { await File('logos/${business.id}.png').delete(); } catch (e) { print('Failure to delete logo! $e'); } if (logoResponse.headers.toString().contains('image/png')) { await File('logos/${business.id}.png') .writeAsBytes(logoResponse.bodyBytes); } return Response.ok( business.id.toString(), headers: {'Access-Control-Allow-Origin': '*'}, ); } on JWTExpiredException { print('JWT Expired'); } on JWTException catch (e) { print(e.message); } return Response.unauthorized( 'unauthorized', headers: {'Access-Control-Allow-Origin': '*'}, ); }); app.post('/fbla-api/signin', (Request request) async { print('signin request received'); final payload = await request.readAsString(); var json = jsonDecode(payload); var username = json['username']; var password = json['password']; var saltDb = await postgres .query('SELECT salt FROM users WHERE username=\'$username\''); if (saltDb.isEmpty) { return Response.unauthorized( 'invalid username', headers: {'Access-Control-Allow-Origin': '*'}, ); } var saltString = saltDb[0][0].toString(); var salt = saltString.toBytesLatin1(); var parameters = Argon2Parameters( Argon2Parameters.ARGON2_i, salt, version: Argon2Parameters.ARGON2_VERSION_10, iterations: 2, memoryPowerOf2: 16, ); var argon2 = Argon2BytesGenerator(); argon2.init(parameters); var passwordBytes = parameters.converter.convert(password); var result = Uint8List(32); argon2.generateBytes(passwordBytes, result); var resultHex = result.toHexString(); var passwordHashDb = await postgres .query('SELECT password_hash FROM users WHERE username=\'$username\''); var passwordHash = passwordHashDb[0][0].toString(); if (passwordHash == resultHex) { final jwt = JWT( {'username': username}, ); final token = jwt.sign(secretKey); try { JWT.verify(token, secretKey); } on JWTExpiredException { print('JWT Expired'); } on JWTException catch (e) { print(e.message); } return Response.ok( token.toString(), headers: {'Access-Control-Allow-Origin': '*'}, ); } else { return Response.unauthorized( 'invalid password', headers: {'Access-Control-Allow-Origin': '*'}, ); } }); app.post('/fbla-api/createuser', (Request request) async { print('create user request received'); var auth = request.headers['Authorization']?.replaceAll('Bearer ', ''); final payload = await request.readAsString(); try { JWT.verify(auth!, secretKey); var json = jsonDecode(payload); var username = json['username']; var password = json['password']; var r = Random.secure(); String randomSalt = String.fromCharCodes( List.generate(32, (index) => r.nextInt(33) + 89)); final salt = randomSalt.toBytesLatin1(); var parameters = Argon2Parameters( Argon2Parameters.ARGON2_i, salt, version: Argon2Parameters.ARGON2_VERSION_10, iterations: 2, memoryPowerOf2: 16, ); var argon2 = Argon2BytesGenerator(); argon2.init(parameters); var passwordBytes = parameters.converter.convert(password); var result = Uint8List(32); argon2.generateBytes(passwordBytes, result); var resultHex = result.toHexString(); postgres.query(''' INSERT INTO public.users (username, password_hash, salt) VALUES ('$username', '$resultHex', '$randomSalt') '''); return Response.ok( username, headers: {'Access-Control-Allow-Origin': '*'}, ); } on JWTExpiredException { print('JWT Expired'); } on JWTException catch (e) { print(e.message); } return Response.unauthorized( 'unauthorized', headers: {'Access-Control-Allow-Origin': '*'}, ); }); app.post('/fbla-api/deleteuser', (Request request) async { print('delete user request received'); var auth = request.headers['Authorization']?.replaceAll('Bearer ', ''); final payload = await request.readAsString(); try { JWT.verify(auth!, secretKey); var json = jsonDecode(payload); var username = json['username']; postgres.query(''' DELETE FROM public.users WHERE username IN ('$username'); '''); return Response.ok( username, headers: {'Access-Control-Allow-Origin': '*'}, ); } on JWTExpiredException { print('JWT Expired'); } on JWTException catch (e) { print(e.message); } return Response.unauthorized( 'unauthorized', headers: {'Access-Control-Allow-Origin': '*'}, ); }); app.get('/fbla-api/marinodev', (Request request) async { print('marinodev request received'); var logo = File('MarinoDev.svg'); List content = logo.readAsBytesSync(); return Response.ok( content, headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'image/svg+xml' }, ); }); // get ip address for hosting for (var interface in await NetworkInterface.list()) { for (var addr in interface.addresses) { if (addr.type == InternetAddressType.IPv4) { _hostname = addr.address; } } } final server = await io.serve(app, _hostname, _port); print('Serving at http://${server.address.host}:${server.port}'); }