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 { cashier, server, mechanic } 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'], 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'], ); } } class JobListing { String name; String description; JobType type; String wage; String link; JobListing({ required this.name, required this.description, required this.type, required this.wage, required this.link, }); } 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(); // 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', (Request request) async { print('business overview request received'); var filters = request.url.queryParameters['filters']?.split(',') ?? JobType.values.asNameMap().keys; // List>>> this is the real type lol List 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 ) ) FROM public.businesses WHERE id IN (SELECT id FROM public.listings WHERE type='${filters.elementAt(i)}') '''))[0][0]; if (postgresResult != null) { output.add({filters.elementAt(i): postgresResult}); } } return Response.ok( json.encode(output), 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', json_agg( json_build_object( 'id', l.id, 'name', l.name, 'description', l.description, 'type', l.type, 'wage', l.wage, 'link', l.link ) ) ) FROM businesses b LEFT JOIN listings l ON b.id = l.business_id 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', (Request request) async { print('business data request received'); final output = await fetchBusinessData(); return Response.ok( output.toString(), 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'); 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}'); // print((await postgres.query('select testdouble from public.test'))); }