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, } enum JobType { retail, customerService, foodService, finance, healthcare, education, maintenance, manufacturing, other, } enum OfferType { job, internship, apprenticeship } 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, this.type, this.website, this.contactName, this.contactEmail, this.contactPhone, this.notes, this.locationName, 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'], website: json['website'], type: typeValid ? BusinessType.values.byName(json['type']) : BusinessType.other, contactName: json['contactName'], contactEmail: json['contactEmail'], contactPhone: json['contactPhone'], notes: json['notes'], locationName: json['locationName'], locationAddress: json['locationAddress'], ); } } class JobListing { int? id; int? businessId; String name; String description; JobType type; String? wage; String? link; OfferType offerType; JobListing( {this.id, this.businessId, required this.name, required this.description, required this.type, this.wage, this.link, required this.offerType}); factory JobListing.fromJson(Map json) { bool typeValid = true; try { JobType.values.byName(json['type']); } catch (e) { typeValid = false; } return JobListing( id: json['id'], businessId: json['businessId'], name: json['name'], description: json['description'], type: typeValid ? JobType.values.byName(json['type']) : JobType.other, wage: json['wage'], link: json['link'], offerType: OfferType.values.byName(json['offerType'])); } } 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[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(); // CORS preflight acceptor app.options('/', (Request request) { return Response.ok(null, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', 'Access-Control-Allow-Headers': '*', }); }); // 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/overview/jobs', (Request request) async { print('business overview request received'); var typeFilters = request.url.queryParameters['typeFilters']?.split(',') ?? JobType.values.asNameMap().keys; var offerFilters = request.url.queryParameters['offerFilters']?.split(',') ?? OfferType.values.asNameMap().keys; var postgresResult = (await postgres.query(''' SELECT jsonb_agg( jsonb_build_object( 'id', b.id, 'name', b.name, 'contactName', b."contactName", 'contactEmail', b."contactEmail", 'contactPhone', b."contactPhone", 'locationName', b."locationName", 'locationAddress', b."locationAddress", 'listings', b.listings ) ) AS result FROM ( SELECT businesses.id, businesses.name, businesses."contactName", businesses."contactEmail", businesses."contactPhone", businesses."locationName", businesses."locationAddress", jsonb_agg( jsonb_build_object( 'id', listings.id, 'name', listings.name, 'description', listings.description, 'type', listings.type, 'wage', listings.wage, 'link', listings.link, 'offerType', listings."offerType" ) ) AS listings FROM businesses JOIN listings ON businesses.id = listings."businessId" AND listings.type IN (${typeFilters.map((element) => "'$element'").join(',')}) AND listings."offerType" IN (${offerFilters.map((element) => "'$element'").join(',')}) GROUP BY businesses.id, businesses.name, businesses."contactName", businesses."contactEmail", businesses."contactPhone", businesses."locationName", businesses."locationAddress" ) b WHERE b.listings IS NOT NULL; ''')); return Response.ok( json.encode(postgresResult[0][0]), headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'text/plain' }, ); }); app.get('/fbla-api/businessdata/overview/types', (Request request) async { print('business overview request received'); var filters = request.url.queryParameters['filters']?.split(',') ?? BusinessType.values.asNameMap().keys; // List>>> this is the real type lol Map output = {}; for (int i = 0; i < filters.length; i++) { var postgresResult = (await postgres.query(''' SELECT json_agg( json_build_object( 'id', id, 'name', name, 'description', description, 'website', website, 'contactName', "contactName", 'contactEmail', "contactEmail", 'contactPhone', "contactPhone", 'locationName', "locationName", 'locationAddress', "locationAddress" ) ) FROM public.businesses WHERE type='${filters.elementAt(i)}' '''))[0][0]; if (postgresResult != null) { output.addAll({filters.elementAt(i): postgresResult}); } } return Response.ok( json.encode(output), headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'text/plain' }, ); }); app.get('/fbla-api/businessdata/businessnames', (Request request) async { print('business names request received'); var postgresResult = (await postgres.query(''' SELECT json_agg( json_build_object( 'id', id, 'name', name ) ) FROM public.businesses '''))[0][0]; return Response.ok( json.encode(postgresResult), headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'text/plain' }, ); }); app.get('/fbla-api/businessdata/business/', (Request request, String business) async { print('idividual business data request received'); var result = (await postgres.query(''' SELECT json_build_object( 'id', b.id, 'name', b.name, 'description', b.description, 'type', b.type, 'website', b.website, 'contactName', b."contactName", 'contactEmail', b."contactEmail", 'contactPhone', b."contactPhone", 'notes', b.notes, 'locationName', b."locationName", 'locationAddress', b."locationAddress", 'listings', CASE WHEN COUNT(l.id) = 0 THEN 'null' ELSE json_agg( json_build_object( 'id', l.id, 'businessId', l."businessId", 'name', l.name, 'description', l.description, 'type', l.type, 'offerType', l."offerType", 'wage', l.wage, 'link', l.link ) ) END ) FROM businesses b LEFT JOIN listings l ON b.id = l."businessId" WHERE b.id = $business GROUP BY b.id; '''))[0][0]; return Response.ok( json.encode(result), headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'text/plain' }, ); }); app.get('/fbla-api/businessdata/businesses', (Request request) async { print('list of business data request received'); if (request.url.queryParameters['businesses'] == null) { return Response.badRequest( body: 'query \'businesses\' required', headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'text/plain' }, ); } var filters = request.url.queryParameters['businesses']!.split(','); var result = (await postgres.query(''' SELECT json_build_object( 'id', b.id, 'name', b.name, 'description', b.description, 'website', b.website, 'type', b.type, 'contactName', b."contactName", 'contactEmail', b."contactEmail", 'contactPhone', b."contactPhone", 'notes', b.notes, 'locationName', b."locationName", 'locationAddress', b."locationAddress" ) FROM businesses b LEFT JOIN listings l ON b.id = l."businessId" WHERE b.id IN (${filters.join(',')}) GROUP BY b.id; ''')); var output = result.map((element) => element[0]).toList(); return Response.ok( json.encode(output), headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'text/plain' }, ); }); app.get('/fbla-api/businessdata', (Request request) async { print('business data request received'); 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[0][0]); return Response.ok( encoded, headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'text/plain' }, ); }); app.get('/fbla-api/logos/', (Request request, String logoId) { print('business logo request received'); var logo = File('logos/$logoId.png'); try { List content = logo.readAsBytesSync(); return Response.ok( content, headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'image/png' }, ); } catch (e) { print('Error reading logo!'); return Response.notFound( 'logo not found', 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, website, type, "contactName", "contactPhone", "contactEmail", notes, "locationName", "locationAddress") VALUES ('${business.name.replaceAll("'", "''")}', '${business.description.replaceAll("'", "''")}', '${business.website ?? 'NULL'}', '${business.type?.name}', '${business.contactName?.replaceAll("'", "''") ?? 'NULL'}', '${business.contactPhone ?? 'NULL'}', '${business.contactEmail ?? 'NULL'}', '${business.notes?.replaceAll("'", "''") ?? 'NULL'}', '${business.locationName?.replaceAll("'", "''") ?? 'NULL'}', '${business.locationAddress?.replaceAll("'", "''") ?? 'NULL'}') ''' .replaceAll("'null'", 'NULL')); 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/createlisting', (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); JobListing listing = JobListing.fromJson(json); await postgres.query(''' INSERT INTO listings ("businessId", name, description, type, wage, link, "offerType") VALUES ('${listing.businessId}', '${listing.name.replaceAll("'", "''")}', '${listing.description.replaceAll("'", "''")}', '${listing.type.name}', '${listing.wage ?? 'NULL'}', '${listing.link?.replaceAll("'", "''") ?? 'NULL'}', '${listing.offerType.name}') ''' .replaceAll("'null'", 'NULL')); final dbListing = await postgres.query('''SELECT id FROM public.listings ORDER BY id DESC LIMIT 1'''); var id = dbListing[0][0]; 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=$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/deletelisting', (Request request) async { print('delete listing 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.listings WHERE id=$id;'); 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, "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}; ''' .replaceAll("'null'", 'NULL')); 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/editlisting', (Request request) async { print('edit listing request received'); final payload = await request.readAsString(); var auth = request.headers['Authorization']?.replaceAll('Bearer ', ''); try { JWT.verify(auth!, secretKey); var json = jsonDecode(payload); JobListing listing = JobListing.fromJson(json); await postgres.query(''' UPDATE listings SET "businessId" = ${listing.businessId}, name = '${listing.name.replaceAll("'", "''")}'::text, description = '${listing.description.replaceAll("'", "''")}'::text, type = '${listing.type.name}'::text, wage = '${listing.wage ?? 'NULL'}'::text, link = '${listing.link?.replaceAll("'", "''") ?? 'NULL'}'::text, "offerType"='${listing.offerType.name}'::text WHERE id = ${listing.id}; ''' .replaceAll("'null'", 'NULL')); return Response.ok( listing.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' }, ); }); app.get('/fbla-api/clearbit/', (Request request, String website) async { print('clearbit logo request received'); website = Uri.decodeComponent(website); var response = await http.get(Uri.parse('https://logo.clearbit.com/$website')); if (response.statusCode == 200) { return Response.ok( response.bodyBytes, headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'image/png' }, ); } else { return Response.notFound( 'logo not found', headers: { 'Access-Control-Allow-Origin': '*', }, ); } }); // 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}'); }