diff --git a/README.md b/README.md index c5b50f0..fcd8f09 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,15 @@ #uImagini ## Running Server - # cd ./cmd/ CONFIG_PATH=$(pwd) DATA_PATH=$(pwd) go run main.go serve ## Generate GraphQL Models - # cd ./cmd/ go run github.com/99designs/gqlgen generate go run main.go generate ## Generate GraphQL Documentation - # From app root graphdoc -e http://localhost:8484/query -o ./docs/schema @@ -20,3 +17,6 @@ ## Flutter Build flutter run + +## Generate GraphQL Flutter Models + flutter pub run build_runner build diff --git a/graph/schema.resolvers.go b/graph/schema.resolvers.go index 08ce5fa..2f30abe 100644 --- a/graph/schema.resolvers.go +++ b/graph/schema.resolvers.go @@ -204,12 +204,16 @@ func (r *queryResolver) Login(ctx context.Context, user string, password string, return &model.AuthResponse{Result: model.AuthResultFailure}, nil } - // Set appropriate cookies - accessCookie = http.Cookie{Name: "AccessToken", Value: accessToken, Path: "/", HttpOnly: true} - refreshCookie = http.Cookie{Name: "RefreshToken", Value: refreshToken, Path: "/", HttpOnly: true} + // Set appropriate cookies (TODO: Only for web!) + accessCookie = http.Cookie{Name: "AccessToken", Value: accessToken, Path: "/", HttpOnly: false} + refreshCookie = http.Cookie{Name: "RefreshToken", Value: refreshToken, Path: "/", HttpOnly: false} http.SetCookie(*resp, &accessCookie) http.SetCookie(*resp, &refreshCookie) + // Only for iOS & Android (TODO: Remove for web! Only cause affected by CORS during development) + (*resp).Header().Set("X-Imagini-AccessToken", accessToken) + (*resp).Header().Set("X-Imagini-RefreshToken", refreshToken) + return &model.AuthResponse{Result: model.AuthResultSuccess, Device: &foundDevice}, nil } diff --git a/internal/api/auth.go b/internal/api/auth.go index 66f4b66..297d6c1 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -50,6 +50,9 @@ func (api *API) refreshTokens(refreshToken jwt.Token) (string, string, error) { } func (api *API) validateTokens(w *http.ResponseWriter, r *http.Request) (jwt.Token, error) { + // TODO: Check from X-Imagini-AccessToken + // TODO: Check from X-Imagini-RefreshToken + // Validate Access Token accessCookie, _ := r.Cookie("AccessToken") if accessCookie != nil { @@ -72,23 +75,29 @@ func (api *API) validateTokens(w *http.ResponseWriter, r *http.Request) (jwt.Tok } // Refresh Access Token & Generate New Refresh Token - newAccessCookie, newRefreshCookie, err := api.refreshTokens(refreshToken) + newAccessToken, newRefreshToken, err := api.refreshTokens(refreshToken) if err != nil { return nil, err } // TODO: Actually Refresh Refresh Token - newRefreshCookie = refreshCookie.Value + newRefreshToken = refreshCookie.Value + + // Set appropriate cookies (TODO: Only for web!) // Update Access & Refresh Cookies http.SetCookie(*w, &http.Cookie{ Name: "AccessToken", - Value: newAccessCookie, + Value: newAccessToken, }) http.SetCookie(*w, &http.Cookie{ Name: "RefreshToken", - Value: newRefreshCookie, + Value: newRefreshToken, }) - return jwt.ParseBytes([]byte(newAccessCookie)) + // Only for iOS & Android (TODO: Remove for web! Only cause affected by CORS during development) + (*w).Header().Set("X-Imagini-AccessToken", newAccessToken) + (*w).Header().Set("X-Imagini-RefreshToken", newRefreshToken) + + return jwt.ParseBytes([]byte(newAccessToken)) } diff --git a/internal/api/middlewares.go b/internal/api/middlewares.go index a845e24..e7d6651 100644 --- a/internal/api/middlewares.go +++ b/internal/api/middlewares.go @@ -24,8 +24,16 @@ func multipleMiddleware(h http.HandlerFunc, m ...Middleware) http.HandlerFunc { * This is used for the graphQL endpoints that may require access to the * Request and ResponseWriter variables. These are used to get / set cookies. **/ -func (api *API) contextMiddleware(next http.Handler) http.Handler { +func (api *API) queryMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // TODO: REMOVE (SOME OF) THIS!! Only for developement due to CORS + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Expose-Headers", "*") + w.Header().Set("Access-Control-Allow-Headers", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") + authContext := &model.AuthContext{ AuthResponse: &w, AuthRequest: r, diff --git a/internal/api/routes.go b/internal/api/routes.go index 265c9fc..f4bffea 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -26,7 +26,7 @@ func (api *API) registerRoutes() { // Handle GraphQL api.Router.Handle("/playground", playground.Handler("GraphQL playground", "/query")) - api.Router.Handle("/query", api.contextMiddleware(srv)) + api.Router.Handle("/query", api.queryMiddleware(srv)) // Handle Resource Route api.Router.HandleFunc("/media/", multipleMiddleware( diff --git a/web_native/build.yaml b/web_native/build.yaml new file mode 100644 index 0000000..6a6f1c5 --- /dev/null +++ b/web_native/build.yaml @@ -0,0 +1,22 @@ +targets: + $default: + sources: + - lib/** + - graphql/** + - schema.graphqls + builders: + artemis: + options: + schema_mapping: + - schema: schema.graphqls + queries_glob: graphql/*.graphql + output: lib/graphql/imgagini_graphql.dart + scalar_mapping: + - graphql_type: Upload + custom_parser_import: 'package:imagini/helpers/upload_serializer.dart' + dart_type: + name: MultipartFile + imports: + - 'package:http/http.dart' + - graphql_type: Time + dart_type: DateTime diff --git a/web_native/graphql/create_mediaitem.graphql b/web_native/graphql/create_mediaitem.graphql new file mode 100644 index 0000000..57934a2 --- /dev/null +++ b/web_native/graphql/create_mediaitem.graphql @@ -0,0 +1,16 @@ +mutation createMediaItem($file: Upload!){ + createMediaItem(input: { + file: $file + }){ + id + createdAt + updatedAt + exifDate + latitude + longitude + isVideo + fileName + origName + userID + } +} diff --git a/web_native/graphql/login.graphql b/web_native/graphql/login.graphql new file mode 100644 index 0000000..c884bff --- /dev/null +++ b/web_native/graphql/login.graphql @@ -0,0 +1,8 @@ +query login($user: String!, $password: String!, $deviceID: ID) { + login(user: $user, password: $password, deviceID: $deviceID) { + result, + device { + id + } + } +} diff --git a/web_native/ios/Flutter/Debug.xcconfig b/web_native/ios/Flutter/Debug.xcconfig index e8efba1..b2f5fae 100644 --- a/web_native/ios/Flutter/Debug.xcconfig +++ b/web_native/ios/Flutter/Debug.xcconfig @@ -1,2 +1,3 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/web_native/ios/Flutter/Release.xcconfig b/web_native/ios/Flutter/Release.xcconfig index 399e934..88c2914 100644 --- a/web_native/ios/Flutter/Release.xcconfig +++ b/web_native/ios/Flutter/Release.xcconfig @@ -1,2 +1,3 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/web_native/lib/api/APIProvider.dart b/web_native/lib/api/api_provider.dart similarity index 52% rename from web_native/lib/api/APIProvider.dart rename to web_native/lib/api/api_provider.dart index 4cd07f0..8db8c68 100644 --- a/web_native/lib/api/APIProvider.dart +++ b/web_native/lib/api/api_provider.dart @@ -1,19 +1,25 @@ -import 'dart:io'; import 'dart:async'; import 'dart:convert'; -import 'package:meta/meta.dart'; -import 'package:http/http.dart' as http; +// ignore: uri_does_not_exist +import 'cookie_client_stub.dart' + // ignore: uri_does_not_exist + if (dart.library.html) 'browser_cookie_client.dart' + // ignore: uri_does_not_exist + if (dart.library.io) 'io_cookie_client.dart'; -import 'package:imagini/models/api/response/LoginResponse.dart'; +import 'package:meta/meta.dart'; +import 'package:graphql_flutter/graphql_flutter.dart'; + +import 'package:imagini/models/api/response/login_response.dart'; class APIProvider{ String _server, _accessToken, _refreshToken; - static const String _LOGIN_API = "/api/v1/Login"; - static const String _MEDIA_ITEMS_API = "/api/v1/MediaItems"; - static const String _ALBUMS_API = "/api/v1/Albums"; - static const String _TAGS_API = "/api/v1/Tags"; + GraphQLClient _client; + HttpLink httpLink; + // CookieLink cookieLink; + static const String _GRAPHQL_ENDPOINT = "/query"; APIProvider({ @required String server, @@ -23,9 +29,27 @@ class APIProvider{ _server = server; _accessToken = accessToken; _refreshToken = refreshToken; + httpLink = HttpLink(_server + _GRAPHQL_ENDPOINT, + httpClient: getCookieClient(), + ); + + // cookieLink = CookieLink(_updateAccessToken, _updateRefreshToken); + _client = GraphQLClient( + cache: GraphQLCache(), + link: httpLink, + ); } - // Endpoint: /api/v1/Login + // void _updateAccessToken(_accessToken) { + // print("Updating Access Token: $_accessToken"); + // this._accessToken = _accessToken; + // } + + // void _updateRefreshToken(_refreshToken) { + // print("Updating Refresh Token: $_accessToken"); + // this._refreshToken = _refreshToken; + // } + Future login([ String username, String password, @@ -34,37 +58,28 @@ class APIProvider{ (username != null && password != null) || (_accessToken != null && _refreshToken != null) ); + String loginQuery = """ + query login(\$user: String!, \$password: String!, \$deviceID: ID) { + login(user: \$user, password: \$password, deviceID: \$deviceID) { + result, + device { + id + } + } + }"""; - http.Response response = await http.post( - Uri.encodeFull(_server + _LOGIN_API), - body: jsonEncode({ - 'user': username, - 'password': password, - }), - ).timeout(Duration(seconds: 10)); - if (response.statusCode != 200) { - // Fuck - return LoginResponse.fromJson(jsonDecode(response.body)); - } + QueryResult response = await _client.query( + QueryOptions( + document: gql(loginQuery), + variables: { + "user": "admin", + "password": "admin" + }, + ) + ); - // Success - String setCookieVal = response.headers["set-cookie"]; - List allCookies = setCookieVal.split(',') - .map((cookie) => Cookie.fromSetCookieValue(cookie)).toList(); - - Cookie accessToken = allCookies.firstWhere((cookie) => cookie.name == "AccessToken"); - Cookie refreshToken = allCookies.firstWhere((cookie) => cookie.name == "RefreshToken"); - - _accessToken = accessToken.toString(); - _refreshToken = refreshToken.toString(); - - print("Status: ${response.statusCode.toString()}"); - print("Body: ${response.body.toString()}"); - print("AccessToken: ${accessToken.toString()}"); - print("RefreshToken: ${refreshToken.toString()}"); - - return LoginResponse.fromJson(jsonDecode(response.body)); + return LoginResponse.fromJson(jsonDecode("{}")); } void logout() { diff --git a/web_native/lib/api/browser_cookie_client.dart b/web_native/lib/api/browser_cookie_client.dart new file mode 100644 index 0000000..8e8de01 --- /dev/null +++ b/web_native/lib/api/browser_cookie_client.dart @@ -0,0 +1,30 @@ +import 'package:http/browser_client.dart'; +import "package:http/http.dart"; + +BaseClient getCookieClient() => ClientWithCookies(); + +class ClientWithCookies extends BrowserClient { + String _accessToken = "asdasdasd"; + String _refreshToken; + + @override + Future send(BaseRequest request) async { + request.headers.addAll({ + 'X-Imagini-AccessToken': _accessToken, + 'X-Imagini-RefreshToken': _refreshToken, + }); + + return super.send(request).then((response) { + if (response.headers.containsKey("x-imagini-accesstoken")) { + this._accessToken = response.headers["x-imagini-accesstoken"]; + } + if (response.headers.containsKey("x-imagini-refreshtoken")) { + this._refreshToken = response.headers["x-imagini-refreshtoken"]; + } + + print("Access Token: $_accessToken"); + print("Refresh Token: $_refreshToken"); + return response; + }); + } +} diff --git a/web_native/lib/api/cookie_client_stub.dart b/web_native/lib/api/cookie_client_stub.dart new file mode 100644 index 0000000..216be4a --- /dev/null +++ b/web_native/lib/api/cookie_client_stub.dart @@ -0,0 +1,4 @@ +import 'package:http/http.dart'; + +BaseClient getCookieClient() => throw UnsupportedError( + 'Cannot create a client without dart:html or dart:io.'); diff --git a/web_native/lib/api/ImaginiAPIRepository.dart b/web_native/lib/api/imagini_api_repository.dart similarity index 66% rename from web_native/lib/api/ImaginiAPIRepository.dart rename to web_native/lib/api/imagini_api_repository.dart index 51ac20f..93da23f 100644 --- a/web_native/lib/api/ImaginiAPIRepository.dart +++ b/web_native/lib/api/imagini_api_repository.dart @@ -1,5 +1,5 @@ -import 'package:imagini/api/APIProvider.dart'; -import 'package:imagini/models/api/response/LoginResponse.dart'; +import 'package:imagini/api/api_provider.dart'; +import 'package:imagini/models/api/response/login_response.dart'; class ImaginiAPIRepository { APIProvider _apiProvider; diff --git a/web_native/lib/api/io_cookie_client.dart b/web_native/lib/api/io_cookie_client.dart new file mode 100644 index 0000000..f9e954f --- /dev/null +++ b/web_native/lib/api/io_cookie_client.dart @@ -0,0 +1,28 @@ +import 'package:http/io_client.dart'; +import "package:http/http.dart"; + +BaseClient getCookieClient() => IOClientWithCookies(); + +class IOClientWithCookies extends IOClient { + String _accessToken; + String _refreshToken; + + @override + Future send(BaseRequest request) async { + // String cookie = await getCookie(); + // String getCookieString(String _) => cookie; + // request.headers.update('cookie', getCookieString); + return super.send(request).then((response) { + if (response.headers.containsKey("x-imagini-accesstoken")) { + this._accessToken = response.headers["x-imagini-accesstoken"]; + } + if (response.headers.containsKey("x-imagini-refreshtoken")) { + this._refreshToken = response.headers["x-imagini-refreshtoken"]; + } + + print("Access Token: $_accessToken"); + print("Refresh Token: $_refreshToken"); + return response; + }); + } +} diff --git a/web_native/lib/blocs/SplashBloc.dart b/web_native/lib/blocs/splash_bloc.dart similarity index 77% rename from web_native/lib/blocs/SplashBloc.dart rename to web_native/lib/blocs/splash_bloc.dart index 0c39bf9..739a0bc 100644 --- a/web_native/lib/blocs/SplashBloc.dart +++ b/web_native/lib/blocs/splash_bloc.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:imagini/core/ImaginiApplication.dart'; -import 'package:imagini/api/ImaginiAPIRepository.dart'; +import 'package:imagini/core/imagini_application.dart'; +import 'package:imagini/api/imagini_api_repository.dart'; -import 'package:imagini/models/api/response/LoginResponse.dart'; +import 'package:imagini/models/api/response/login_response.dart'; class SplashBloc{ diff --git a/web_native/lib/core/Env.dart b/web_native/lib/core/Env.dart index 5b87b63..07b65ae 100644 --- a/web_native/lib/core/Env.dart +++ b/web_native/lib/core/Env.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:imagini/core/AppComponent.dart'; -import 'package:imagini/core/ImaginiApplication.dart'; +import 'package:imagini/core/app_component.dart'; +import 'package:imagini/core/imagini_application.dart'; class Env { static Env value; diff --git a/web_native/lib/core/AppComponent.dart b/web_native/lib/core/app_component.dart similarity index 87% rename from web_native/lib/core/AppComponent.dart rename to web_native/lib/core/app_component.dart index d7ff1e2..7f785f5 100644 --- a/web_native/lib/core/AppComponent.dart +++ b/web_native/lib/core/app_component.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:imagini/core/Env.dart'; -import 'package:imagini/core/AppProvider.dart'; -import 'package:imagini/core/ImaginiApplication.dart'; +import 'package:imagini/core/env.dart'; +import 'package:imagini/core/app_provider.dart'; +import 'package:imagini/core/imagini_application.dart'; class AppComponent extends StatefulWidget { diff --git a/web_native/lib/core/AppProvider.dart b/web_native/lib/core/app_provider.dart similarity index 92% rename from web_native/lib/core/AppProvider.dart rename to web_native/lib/core/app_provider.dart index 6efc539..f13b7d1 100644 --- a/web_native/lib/core/AppProvider.dart +++ b/web_native/lib/core/app_provider.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:fluro/fluro.dart'; -import 'package:imagini/core/ImaginiApplication.dart'; +import 'package:imagini/core/imagini_application.dart'; class AppProvider extends InheritedWidget { diff --git a/web_native/lib/core/AppRoutes.dart b/web_native/lib/core/app_routes.dart similarity index 90% rename from web_native/lib/core/AppRoutes.dart rename to web_native/lib/core/app_routes.dart index da3dbb0..3127066 100644 --- a/web_native/lib/core/AppRoutes.dart +++ b/web_native/lib/core/app_routes.dart @@ -1,9 +1,9 @@ import 'package:fluro/fluro.dart'; import 'package:flutter/material.dart'; -import 'package:imagini/screens/HomeScreen.dart'; -import 'package:imagini/screens/LoginScreen.dart'; -import 'package:imagini/screens/SplashScreen.dart'; +import 'package:imagini/screens/home_screen.dart'; +import 'package:imagini/screens/login_screen.dart'; +import 'package:imagini/screens/splash_screen.dart'; var splashHandler = new Handler( handlerFunc: (BuildContext context, Map> params) { diff --git a/web_native/lib/core/ImaginiApplication.dart b/web_native/lib/core/imagini_application.dart similarity index 75% rename from web_native/lib/core/ImaginiApplication.dart rename to web_native/lib/core/imagini_application.dart index fff3c2c..1834ba1 100644 --- a/web_native/lib/core/ImaginiApplication.dart +++ b/web_native/lib/core/imagini_application.dart @@ -1,8 +1,8 @@ import 'package:fluro/fluro.dart'; -import 'package:imagini/core/AppRoutes.dart'; -import 'package:imagini/api/APIProvider.dart'; -import 'package:imagini/api/ImaginiAPIRepository.dart'; +import 'package:imagini/core/app_routes.dart'; +import 'package:imagini/api/api_provider.dart'; +import 'package:imagini/api/imagini_api_repository.dart'; class ImaginiApplication { FluroRouter router; @@ -21,7 +21,7 @@ class ImaginiApplication { } _initAPIRepository() { - APIProvider apiProvider = new APIProvider(server: "http://10.0.20.170:8484"); + APIProvider apiProvider = new APIProvider(server: "http://localhost:8484"); imaginiAPI = ImaginiAPIRepository(apiProvider); } } diff --git a/web_native/lib/graphql/imgagini_graphql.dart b/web_native/lib/graphql/imgagini_graphql.dart new file mode 100644 index 0000000..379ac10 --- /dev/null +++ b/web_native/lib/graphql/imgagini_graphql.dart @@ -0,0 +1,2 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +export 'imgagini_graphql.graphql.dart'; diff --git a/web_native/lib/graphql/imgagini_graphql.graphql.dart b/web_native/lib/graphql/imgagini_graphql.graphql.dart new file mode 100644 index 0000000..367b020 --- /dev/null +++ b/web_native/lib/graphql/imgagini_graphql.graphql.dart @@ -0,0 +1,354 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +import 'package:meta/meta.dart'; +import 'package:artemis/artemis.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:equatable/equatable.dart'; +import 'package:gql/ast.dart'; +import 'package:http/http.dart'; +import 'package:imagini/helpers/upload_serializer.dart'; +part 'imgagini_graphql.graphql.g.dart'; + +@JsonSerializable(explicitToJson: true) +class Login$Query$AuthResponse$Device with EquatableMixin { + Login$Query$AuthResponse$Device(); + + factory Login$Query$AuthResponse$Device.fromJson(Map json) => + _$Login$Query$AuthResponse$DeviceFromJson(json); + + String id; + + @override + List get props => [id]; + Map toJson() => + _$Login$Query$AuthResponse$DeviceToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class Login$Query$AuthResponse with EquatableMixin { + Login$Query$AuthResponse(); + + factory Login$Query$AuthResponse.fromJson(Map json) => + _$Login$Query$AuthResponseFromJson(json); + + @JsonKey(unknownEnumValue: AuthResult.artemisUnknown) + AuthResult result; + + Login$Query$AuthResponse$Device device; + + @override + List get props => [result, device]; + Map toJson() => _$Login$Query$AuthResponseToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class Login$Query with EquatableMixin { + Login$Query(); + + factory Login$Query.fromJson(Map json) => + _$Login$QueryFromJson(json); + + Login$Query$AuthResponse login; + + @override + List get props => [login]; + Map toJson() => _$Login$QueryToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class CreateMediaItem$Mutation$MediaItem with EquatableMixin { + CreateMediaItem$Mutation$MediaItem(); + + factory CreateMediaItem$Mutation$MediaItem.fromJson( + Map json) => + _$CreateMediaItem$Mutation$MediaItemFromJson(json); + + String id; + + DateTime createdAt; + + DateTime updatedAt; + + DateTime exifDate; + + double latitude; + + double longitude; + + bool isVideo; + + String fileName; + + String origName; + + String userID; + + @override + List get props => [ + id, + createdAt, + updatedAt, + exifDate, + latitude, + longitude, + isVideo, + fileName, + origName, + userID + ]; + Map toJson() => + _$CreateMediaItem$Mutation$MediaItemToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class CreateMediaItem$Mutation with EquatableMixin { + CreateMediaItem$Mutation(); + + factory CreateMediaItem$Mutation.fromJson(Map json) => + _$CreateMediaItem$MutationFromJson(json); + + CreateMediaItem$Mutation$MediaItem createMediaItem; + + @override + List get props => [createMediaItem]; + Map toJson() => _$CreateMediaItem$MutationToJson(this); +} + +enum AuthResult { + @JsonValue('Success') + success, + @JsonValue('Failure') + failure, + @JsonValue('ARTEMIS_UNKNOWN') + artemisUnknown, +} + +@JsonSerializable(explicitToJson: true) +class LoginArguments extends JsonSerializable with EquatableMixin { + LoginArguments({@required this.user, @required this.password, this.deviceID}); + + @override + factory LoginArguments.fromJson(Map json) => + _$LoginArgumentsFromJson(json); + + final String user; + + final String password; + + final String deviceID; + + @override + List get props => [user, password, deviceID]; + @override + Map toJson() => _$LoginArgumentsToJson(this); +} + +class LoginQuery extends GraphQLQuery { + LoginQuery({this.variables}); + + @override + final DocumentNode document = DocumentNode(definitions: [ + OperationDefinitionNode( + type: OperationType.query, + name: NameNode(value: 'login'), + variableDefinitions: [ + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'user')), + type: NamedTypeNode( + name: NameNode(value: 'String'), isNonNull: true), + defaultValue: DefaultValueNode(value: null), + directives: []), + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'password')), + type: NamedTypeNode( + name: NameNode(value: 'String'), isNonNull: true), + defaultValue: DefaultValueNode(value: null), + directives: []), + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'deviceID')), + type: + NamedTypeNode(name: NameNode(value: 'ID'), isNonNull: false), + defaultValue: DefaultValueNode(value: null), + directives: []) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'login'), + alias: null, + arguments: [ + ArgumentNode( + name: NameNode(value: 'user'), + value: VariableNode(name: NameNode(value: 'user'))), + ArgumentNode( + name: NameNode(value: 'password'), + value: VariableNode(name: NameNode(value: 'password'))), + ArgumentNode( + name: NameNode(value: 'deviceID'), + value: VariableNode(name: NameNode(value: 'deviceID'))) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'result'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'device'), + alias: null, + arguments: [], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'id'), + alias: null, + arguments: [], + directives: [], + selectionSet: null) + ])) + ])) + ])) + ]); + + @override + final String operationName = 'login'; + + @override + final LoginArguments variables; + + @override + List get props => [document, operationName, variables]; + @override + Login$Query parse(Map json) => Login$Query.fromJson(json); +} + +@JsonSerializable(explicitToJson: true) +class CreateMediaItemArguments extends JsonSerializable with EquatableMixin { + CreateMediaItemArguments({@required this.file}); + + @override + factory CreateMediaItemArguments.fromJson(Map json) => + _$CreateMediaItemArgumentsFromJson(json); + + @JsonKey( + fromJson: fromGraphQLUploadToDartMultipartFile, + toJson: fromDartMultipartFileToGraphQLUpload) + final MultipartFile file; + + @override + List get props => [file]; + @override + Map toJson() => _$CreateMediaItemArgumentsToJson(this); +} + +class CreateMediaItemMutation + extends GraphQLQuery { + CreateMediaItemMutation({this.variables}); + + @override + final DocumentNode document = DocumentNode(definitions: [ + OperationDefinitionNode( + type: OperationType.mutation, + name: NameNode(value: 'createMediaItem'), + variableDefinitions: [ + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'file')), + type: NamedTypeNode( + name: NameNode(value: 'Upload'), isNonNull: true), + defaultValue: DefaultValueNode(value: null), + directives: []) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'createMediaItem'), + alias: null, + arguments: [ + ArgumentNode( + name: NameNode(value: 'input'), + value: ObjectValueNode(fields: [ + ObjectFieldNode( + name: NameNode(value: 'file'), + value: VariableNode(name: NameNode(value: 'file'))) + ])) + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'id'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'createdAt'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'updatedAt'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'exifDate'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'latitude'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'longitude'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'isVideo'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'fileName'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'origName'), + alias: null, + arguments: [], + directives: [], + selectionSet: null), + FieldNode( + name: NameNode(value: 'userID'), + alias: null, + arguments: [], + directives: [], + selectionSet: null) + ])) + ])) + ]); + + @override + final String operationName = 'createMediaItem'; + + @override + final CreateMediaItemArguments variables; + + @override + List get props => [document, operationName, variables]; + @override + CreateMediaItem$Mutation parse(Map json) => + CreateMediaItem$Mutation.fromJson(json); +} diff --git a/web_native/lib/graphql/imgagini_graphql.graphql.g.dart b/web_native/lib/graphql/imgagini_graphql.graphql.g.dart new file mode 100644 index 0000000..219936f --- /dev/null +++ b/web_native/lib/graphql/imgagini_graphql.graphql.g.dart @@ -0,0 +1,166 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'imgagini_graphql.graphql.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Login$Query$AuthResponse$Device _$Login$Query$AuthResponse$DeviceFromJson( + Map json) { + return Login$Query$AuthResponse$Device()..id = json['id'] as String; +} + +Map _$Login$Query$AuthResponse$DeviceToJson( + Login$Query$AuthResponse$Device instance) => + { + 'id': instance.id, + }; + +Login$Query$AuthResponse _$Login$Query$AuthResponseFromJson( + Map json) { + return Login$Query$AuthResponse() + ..result = _$enumDecodeNullable(_$AuthResultEnumMap, json['result'], + unknownValue: AuthResult.artemisUnknown) + ..device = json['device'] == null + ? null + : Login$Query$AuthResponse$Device.fromJson( + json['device'] as Map); +} + +Map _$Login$Query$AuthResponseToJson( + Login$Query$AuthResponse instance) => + { + 'result': _$AuthResultEnumMap[instance.result], + 'device': instance.device?.toJson(), + }; + +T _$enumDecode( + Map enumValues, + dynamic source, { + T unknownValue, +}) { + if (source == null) { + throw ArgumentError('A value must be provided. Supported values: ' + '${enumValues.values.join(', ')}'); + } + + final value = enumValues.entries + .singleWhere((e) => e.value == source, orElse: () => null) + ?.key; + + if (value == null && unknownValue == null) { + throw ArgumentError('`$source` is not one of the supported values: ' + '${enumValues.values.join(', ')}'); + } + return value ?? unknownValue; +} + +T _$enumDecodeNullable( + Map enumValues, + dynamic source, { + T unknownValue, +}) { + if (source == null) { + return null; + } + return _$enumDecode(enumValues, source, unknownValue: unknownValue); +} + +const _$AuthResultEnumMap = { + AuthResult.success: 'Success', + AuthResult.failure: 'Failure', + AuthResult.artemisUnknown: 'ARTEMIS_UNKNOWN', +}; + +Login$Query _$Login$QueryFromJson(Map json) { + return Login$Query() + ..login = json['login'] == null + ? null + : Login$Query$AuthResponse.fromJson( + json['login'] as Map); +} + +Map _$Login$QueryToJson(Login$Query instance) => + { + 'login': instance.login?.toJson(), + }; + +CreateMediaItem$Mutation$MediaItem _$CreateMediaItem$Mutation$MediaItemFromJson( + Map json) { + return CreateMediaItem$Mutation$MediaItem() + ..id = json['id'] as String + ..createdAt = json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String) + ..updatedAt = json['updatedAt'] == null + ? null + : DateTime.parse(json['updatedAt'] as String) + ..exifDate = json['exifDate'] == null + ? null + : DateTime.parse(json['exifDate'] as String) + ..latitude = (json['latitude'] as num)?.toDouble() + ..longitude = (json['longitude'] as num)?.toDouble() + ..isVideo = json['isVideo'] as bool + ..fileName = json['fileName'] as String + ..origName = json['origName'] as String + ..userID = json['userID'] as String; +} + +Map _$CreateMediaItem$Mutation$MediaItemToJson( + CreateMediaItem$Mutation$MediaItem instance) => + { + 'id': instance.id, + 'createdAt': instance.createdAt?.toIso8601String(), + 'updatedAt': instance.updatedAt?.toIso8601String(), + 'exifDate': instance.exifDate?.toIso8601String(), + 'latitude': instance.latitude, + 'longitude': instance.longitude, + 'isVideo': instance.isVideo, + 'fileName': instance.fileName, + 'origName': instance.origName, + 'userID': instance.userID, + }; + +CreateMediaItem$Mutation _$CreateMediaItem$MutationFromJson( + Map json) { + return CreateMediaItem$Mutation() + ..createMediaItem = json['createMediaItem'] == null + ? null + : CreateMediaItem$Mutation$MediaItem.fromJson( + json['createMediaItem'] as Map); +} + +Map _$CreateMediaItem$MutationToJson( + CreateMediaItem$Mutation instance) => + { + 'createMediaItem': instance.createMediaItem?.toJson(), + }; + +LoginArguments _$LoginArgumentsFromJson(Map json) { + return LoginArguments( + user: json['user'] as String, + password: json['password'] as String, + deviceID: json['deviceID'] as String, + ); +} + +Map _$LoginArgumentsToJson(LoginArguments instance) => + { + 'user': instance.user, + 'password': instance.password, + 'deviceID': instance.deviceID, + }; + +CreateMediaItemArguments _$CreateMediaItemArgumentsFromJson( + Map json) { + return CreateMediaItemArguments( + file: fromGraphQLUploadToDartMultipartFile(json['file'] as MultipartFile), + ); +} + +Map _$CreateMediaItemArgumentsToJson( + CreateMediaItemArguments instance) => + { + 'file': fromDartMultipartFileToGraphQLUpload(instance.file), + }; diff --git a/web_native/lib/helpers/upload_serializer.dart b/web_native/lib/helpers/upload_serializer.dart new file mode 100644 index 0000000..c79d125 --- /dev/null +++ b/web_native/lib/helpers/upload_serializer.dart @@ -0,0 +1,4 @@ +import 'package:http/http.dart'; + +MultipartFile fromGraphQLUploadToDartMultipartFile(MultipartFile file) => file; +MultipartFile fromDartMultipartFileToGraphQLUpload(MultipartFile file) => file; diff --git a/web_native/lib/main.dart b/web_native/lib/main.dart index aa45dc9..897f279 100644 --- a/web_native/lib/main.dart +++ b/web_native/lib/main.dart @@ -1,4 +1,4 @@ -import 'package:imagini/core/Env.dart'; +import 'package:imagini/core/env.dart'; void main() => ProductionImagini(); diff --git a/web_native/lib/models/api/MediaItem.dart b/web_native/lib/models/api/media_item.dart similarity index 100% rename from web_native/lib/models/api/MediaItem.dart rename to web_native/lib/models/api/media_item.dart diff --git a/web_native/lib/models/api/response/AlbumsResponse.dart b/web_native/lib/models/api/response/album_response.dart similarity index 100% rename from web_native/lib/models/api/response/AlbumsResponse.dart rename to web_native/lib/models/api/response/album_response.dart diff --git a/web_native/lib/models/api/response/LoginResponse.dart b/web_native/lib/models/api/response/login_response.dart similarity index 100% rename from web_native/lib/models/api/response/LoginResponse.dart rename to web_native/lib/models/api/response/login_response.dart diff --git a/web_native/lib/models/api/response/MediaItemsResponse.dart b/web_native/lib/models/api/response/media_items_response.dart similarity index 100% rename from web_native/lib/models/api/response/MediaItemsResponse.dart rename to web_native/lib/models/api/response/media_items_response.dart diff --git a/web_native/lib/models/api/response/TagsResponse.dart b/web_native/lib/models/api/response/tags_response.dart similarity index 100% rename from web_native/lib/models/api/response/TagsResponse.dart rename to web_native/lib/models/api/response/tags_response.dart diff --git a/web_native/lib/screens/HomeScreen.dart b/web_native/lib/screens/home_screen.dart similarity index 100% rename from web_native/lib/screens/HomeScreen.dart rename to web_native/lib/screens/home_screen.dart diff --git a/web_native/lib/screens/LoginScreen.dart b/web_native/lib/screens/login_screen.dart similarity index 100% rename from web_native/lib/screens/LoginScreen.dart rename to web_native/lib/screens/login_screen.dart diff --git a/web_native/lib/screens/SplashScreen.dart b/web_native/lib/screens/splash_screen.dart similarity index 100% rename from web_native/lib/screens/SplashScreen.dart rename to web_native/lib/screens/splash_screen.dart diff --git a/web_native/lib/screens/TemplateScreen.dart.template b/web_native/lib/screens/template_screen.dart.template similarity index 100% rename from web_native/lib/screens/TemplateScreen.dart.template rename to web_native/lib/screens/template_screen.dart.template diff --git a/web_native/packages/imagini_api/lib/imagini_api.dart b/web_native/packages/imagini_api/lib/imagini_api.dart deleted file mode 100644 index 17eecc4..0000000 --- a/web_native/packages/imagini_api/lib/imagini_api.dart +++ /dev/null @@ -1,3 +0,0 @@ -library imagini_api; - -export 'src/imagini_api.dart'; diff --git a/web_native/packages/imagini_api/lib/src/imagini_api.dart b/web_native/packages/imagini_api/lib/src/imagini_api.dart deleted file mode 100644 index eb959d1..0000000 --- a/web_native/packages/imagini_api/lib/src/imagini_api.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'dart:io'; -import 'dart:async'; -import 'dart:convert'; -import 'package:http/http.dart' as http; - -import 'package:meta/meta.dart'; - -enum ImaginiState { unknown, authenticated, unauthenticated } - -class ImaginiAPI{ - String _server, _accessToken, _refreshToken; - final _controller = StreamController(); - - ImaginiAPI({ - @required String server, - String accessToken, - String refreshToken - }) { - _server = server; - _accessToken = accessToken; - _refreshToken = refreshToken; - } - - Stream get status async* { - await Future.delayed(const Duration(seconds: 1)); - yield ImaginiState.unauthenticated; - yield* _controller.stream; - } - - // Endpoint: /api/v1/Login - Future login([ - String username, - String password, - ]) async { - assert( - (username != null && password != null) || - (_accessToken != null && _refreshToken != null) - ); - - http.Response response = await http.post( - Uri.encodeFull(_server + "/api/v1/Login"), - body: jsonEncode({ - 'user': username, - 'password': password, - }), - ).timeout(Duration(seconds: 10)); - - if (response.statusCode != 200) { - // Fuck - return; - } - - // Success - String setCookieVal = response.headers["set-cookie"]; - List allCookies = setCookieVal.split(',') - .map((cookie) => Cookie.fromSetCookieValue(cookie)).toList(); - - Cookie accessToken = allCookies.firstWhere((cookie) => cookie.name == "AccessToken"); - Cookie refreshToken = allCookies.firstWhere((cookie) => cookie.name == "RefreshToken"); - - _accessToken = accessToken.toString(); - _refreshToken = refreshToken.toString(); - - _controller.add(ImaginiState.authenticated); - - print("Status: ${response.statusCode.toString()}"); - print("Body: ${response.body.toString()}"); - print("AccessToken: ${accessToken.toString()}"); - print("RefreshToken: ${refreshToken.toString()}"); - } - - void logout() { - _controller.add(ImaginiState.unauthenticated); - } - - Future mediaItems([ - String startDate, - String endDate, - String albumID, - List tagID, - String type, // TODO: Make enum - int page, - ]) async { - // Query: - // /api/v1/MediaItems - // Derive Params: - // startDate: - // &createdAt=>2020-10-10T10:10:10 - // endDate: - // &createdAt=<2020-10-10T10:10:10 - // albumID: - // &albumID=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d - // tagID: - // &tagID=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d,9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d - // type: - // &type=Photos - // &type=Videos - // page: - // &page=4 - - // Returns: - // { - // - // } - - return null; - } - - Future tags([ - int page - ]) async { - // Query: - // /api/v1/Tags - // Derive Params: - // page: - // &page=4 - return null; - } - - Future albums([ - int page - ]) async { - // Query: - // /api/v1/Albums - // Derive Params: - // page: - // &page=4 - return null; - } - - Future me() async { - return null; - } - - void dispose() => _controller.close(); -} diff --git a/web_native/packages/imagini_api/pubspec.yaml b/web_native/packages/imagini_api/pubspec.yaml deleted file mode 100644 index 3e00d8d..0000000 --- a/web_native/packages/imagini_api/pubspec.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: imagini_api -description: Dart package that implements the Imagini API -version: 1.0.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - -dependencies: - meta: ^1.1.8 diff --git a/web_native/pubspec.lock b/web_native/pubspec.lock index ed21a07..7f8dc3b 100644 --- a/web_native/pubspec.lock +++ b/web_native/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "12.0.0" + version: "14.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.40.6" + version: "0.41.2" archive: dependency: transitive description: @@ -29,6 +29,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.0" + artemis: + dependency: "direct main" + description: + name: artemis + url: "https://pub.dartlang.org" + source: hosted + version: "6.18.3" async: dependency: transitive description: @@ -50,6 +57,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0-nullsafety.3" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.2" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.5" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.7" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.3" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "1.11.1" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.7" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.0.0-nullsafety.0" characters: dependency: transitive description: @@ -64,6 +127,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0-nullsafety.3" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" cli_util: dependency: transitive description: @@ -78,6 +148,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0-nullsafety.3" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "3.6.0" collection: dependency: transitive description: @@ -85,6 +162,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0-nullsafety.5" + connectivity: + dependency: transitive + description: + name: connectivity + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + connectivity_for_web: + dependency: transitive + description: + name: connectivity_for_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1+4" + connectivity_macos: + dependency: transitive + description: + name: connectivity_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0+7" + connectivity_platform_interface: + dependency: transitive + description: + name: connectivity_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.6" convert: dependency: transitive description: @@ -92,13 +197,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.1" - coverage: - dependency: transitive - description: - name: coverage - url: "https://pub.dartlang.org" - source: hosted - version: "0.14.2" crypto: dependency: transitive description: @@ -113,13 +211,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.12" equatable: dependency: "direct main" description: name: equatable url: "https://pub.dartlang.org" source: hosted - version: "1.2.5" + version: "1.2.6" fake_async: dependency: transitive description: @@ -147,7 +252,14 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "2.1.6" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" fluro: dependency: "direct main" description: @@ -166,7 +278,7 @@ packages: name: flutter_bloc url: "https://pub.dartlang.org" source: hosted - version: "6.1.1" + version: "6.1.2" flutter_driver: dependency: transitive description: flutter @@ -178,7 +290,7 @@ packages: name: flutter_platform_widgets url: "https://pub.dartlang.org" source: hosted - version: "0.72.0" + version: "0.80.0-beta.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -199,7 +311,7 @@ packages: name: flutter_staggered_grid_view url: "https://pub.dartlang.org" source: hosted - version: "0.3.3" + version: "0.3.4" flutter_test: dependency: "direct dev" description: flutter @@ -222,6 +334,90 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + gql: + dependency: "direct main" + description: + name: gql + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.4" + gql_code_gen: + dependency: transitive + description: + name: gql_code_gen + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" + gql_dedupe_link: + dependency: transitive + description: + name: gql_dedupe_link + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.10" + gql_error_link: + dependency: transitive + description: + name: gql_error_link + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1-alpha+1601131172858" + gql_exec: + dependency: transitive + description: + name: gql_exec + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.5" + gql_http_link: + dependency: transitive + description: + name: gql_http_link + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.2" + gql_link: + dependency: transitive + description: + name: gql_link + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1" + gql_transform_link: + dependency: transitive + description: + name: gql_transform_link + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" + graphql: + dependency: transitive + description: + name: graphql + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + graphql_flutter: + dependency: "direct main" + description: + name: graphql_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + hive: + dependency: transitive + description: + name: hive + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.4+1" http: dependency: "direct main" description: @@ -229,6 +425,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" http_parser: dependency: transitive description: @@ -255,20 +458,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.3-nullsafety.3" - json_rpc_2: - dependency: transitive + json_annotation: + dependency: "direct main" description: - name: json_rpc_2 + name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "2.2.2" + version: "3.1.1" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + url: "https://pub.dartlang.org" + source: hosted + version: "3.5.1" logging: dependency: transitive description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "0.11.4" + version: "1.0.0" matcher: dependency: transitive description: @@ -277,12 +487,19 @@ packages: source: hosted version: "0.12.10-nullsafety.3" meta: - dependency: transitive + dependency: "direct main" description: name: meta url: "https://pub.dartlang.org" source: hosted version: "1.3.0-nullsafety.6" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" nested: dependency: transitive description: @@ -304,6 +521,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.1" + normalize: + dependency: transitive + description: + name: normalize + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.7" package_config: dependency: transitive description: @@ -318,6 +542,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0-nullsafety.3" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.27" path_provider_linux: dependency: transitive description: @@ -325,6 +556,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.1+2" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4+8" path_provider_platform_interface: dependency: transitive description: @@ -345,7 +583,7 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.10.0-nullsafety.3" + version: "1.10.0" platform: dependency: transitive description: @@ -366,7 +604,7 @@ packages: name: pool url: "https://pub.dartlang.org" source: hosted - version: "1.5.0-nullsafety.3" + version: "1.4.0" process: dependency: transitive description: @@ -388,6 +626,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.8" + recase: + dependency: transitive + description: + name: recase + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + rxdart: + dependency: transitive + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.24.1" shared_preferences: dependency: "direct main" description: @@ -429,26 +688,33 @@ packages: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "0.0.2+2" + version: "0.0.2+3" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.9" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.4" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" - source_map_stack_trace: + source_gen: dependency: transitive description: - name: source_map_stack_trace + name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "2.1.0-nullsafety.4" - source_maps: - dependency: transitive - description: - name: source_maps - url: "https://pub.dartlang.org" - source: hosted - version: "0.10.10-nullsafety.3" + version: "0.9.10+1" source_span: dependency: transitive description: @@ -470,6 +736,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0-nullsafety.3" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" string_scanner: dependency: transitive description: @@ -498,13 +771,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.19-nullsafety.6" - test_core: + timing: dependency: transitive description: - name: test_core + name: timing url: "https://pub.dartlang.org" source: hosted - version: "0.3.12-nullsafety.9" + version: "0.1.1+3" typed_data: dependency: transitive description: @@ -546,7 +819,7 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.5+1" + version: "0.1.5+3" url_launcher_windows: dependency: transitive description: @@ -554,6 +827,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.1+3" + uuid_enhanced: + dependency: transitive + description: + name: uuid_enhanced + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" vector_math: dependency: transitive description: @@ -581,7 +861,7 @@ packages: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" webdriver: dependency: transitive description: @@ -589,13 +869,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + websocket: + dependency: transitive + description: + name: websocket + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.5" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "1.7.4" + version: "1.7.4+1" xdg_directories: dependency: transitive description: @@ -612,4 +899,4 @@ packages: version: "2.2.1" sdks: dart: ">=2.12.0-0.0 <3.0.0" - flutter: ">=1.22.0 <2.0.0" + flutter: ">=1.22.0" diff --git a/web_native/pubspec.yaml b/web_native/pubspec.yaml index 0daa3f8..6a8d0a1 100644 --- a/web_native/pubspec.yaml +++ b/web_native/pubspec.yaml @@ -23,23 +23,31 @@ environment: dependencies: flutter: sdk: flutter + graphql_flutter: ^4.0.1 flutter_staggered_grid_view: "^0.3.2" fluro: ^1.7.8 equatable: ^1.2.5 http: ^0.12.2 shared_preferences: ^0.5.12+4 flutter_secure_storage: ^3.3.5 - flutter_platform_widgets: ^0.72.0 + flutter_platform_widgets: ^0.80.0-beta.0 flutter_bloc: ^6.1.1 file_picker: ^2.1.5 url_launcher: ^5.7.10 cupertino_icons: ^1.0.1 + artemis: '>=6.0.0 <7.0.0' # only if you're using ArtemisClient! + json_annotation: ^3.1.0 + meta: '>=1.0.0 <2.0.0' # only if you have non nullable fields + gql: '>=0.12.3 <1.0.0' dev_dependencies: flutter_test: sdk: flutter integration_test: sdk: flutter + artemis: '>=6.0.0 <7.0.0' + build_runner: ^1.10.4 + json_serializable: ^3.5.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/web_native/schema.graphqls b/web_native/schema.graphqls new file mode 100644 index 0000000..8e7bfdb --- /dev/null +++ b/web_native/schema.graphqls @@ -0,0 +1,384 @@ + +# https://gqlgen.com/reference/scalars/ +scalar Time +scalar Upload + +# https://gqlgen.com/reference/directives/ +directive @hasMinRole(role: Role!) on FIELD_DEFINITION +directive @isPrivate on FIELD_DEFINITION | INPUT_FIELD_DEFINITION + +directive @meta( + gorm: String, +) on OBJECT | FIELD_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION | ENUM | INPUT_OBJECT | ARGUMENT_DEFINITION + +enum Role { + Admin + User +} + +enum DeviceType { + iOS + Android + Chrome + Firefox + InternetExplorer + Edge + Safari + Unknown +} + +enum AuthType { + Local + LDAP +} + +enum OrderDirection { + ASC + DESC +} + +# ------------------------------------------------------------ +# ---------------------- Authentication ---------------------- +# ------------------------------------------------------------ + +enum AuthResult { + Success + Failure +} + +type AuthResponse { + result: AuthResult! + device: Device + error: String +} + +# ------------------------------------------------------------ +# ----------------------- Type Filters ----------------------- +# ------------------------------------------------------------ + +input TimeFilter { + equalTo: Time + notEqualTo: Time + lessThan: Time + lessThanOrEqualTo: Time + greaterThan: Time + greaterThanOrEqualTo: Time +} + +input IntFilter { + equalTo: Int + notEqualTo: Int + lessThan: Int + lessThanOrEqualTo: Int + greaterThan: Int + greaterThanOrEqualTo: Int +} + +input FloatFilter { + equalTo: Float + notEqualTo: Float + lessThan: Float + lessThanOrEqualTo: Float + greaterThan: Float + greaterThanOrEqualTo: Float +} + +input BooleanFilter { + equalTo: Boolean + notEqualTo: Boolean +} + +input IDFilter { + equalTo: ID + notEqualTo: ID +} + +input StringFilter { + equalTo: String + notEqualTo: String + startsWith: String + notStartsWith: String + endsWith: String + notEndsWith: String + contains: String + notContains: String +} + +input RoleFilter { + equalTo: Role + notEqualTo: Role +} + +input DeviceTypeFilter { + equalTo: DeviceType + notEqualTo: DeviceType +} + +input AuthTypeFilter { + equalTo: AuthType + notEqualTo: AuthType +} + +# ------------------------------------------------------------ +# -------------------- Object Definitions -------------------- +# ------------------------------------------------------------ + +type User { + id: ID! @meta(gorm: "primaryKey;not null") + createdAt: Time + updatedAt: Time + email: String! @meta(gorm: "not null;unique") + username: String! @meta(gorm: "not null;unique") + firstName: String + lastName: String + role: Role! @meta(gorm: "default:User;not null") + authType: AuthType! @meta(gorm: "default:Local;not null") + password: String @isPrivate + devices: [Device!] @meta(gorm: "foreignKey:UserID") + mediaItems: [MediaItem!] @meta(gorm: "foreignKey:UserID") +} + +type Device { + id: ID! @meta(gorm: "primaryKey;not null") + createdAt: Time + updatedAt: Time + name: String! @meta(gorm: "not null") + type: DeviceType! @meta(gorm: "default:Unknown;not null") + refreshKey: String @isPrivate + userID: ID! @meta(gorm: "not null") +} + +type MediaItem { + id: ID! @meta(gorm: "primaryKey;not null") + createdAt: Time + updatedAt: Time + exifDate: Time + latitude: Float @meta(gorm: "precision:5") + longitude: Float @meta(gorm: "precision:5") + isVideo: Boolean! @meta(gorm: "default:false;not null") + fileName: String! @meta(gorm: "not null") + origName: String! @meta(gorm: "not null") + tags: [Tag] @meta(gorm: "many2many:media_tags;foreignKey:ID,UserID;References:ID") + albums: [Album] @meta(gorm: "many2many:media_albums;foreignKey:ID,UserID;Refrences:ID") + userID: ID! @meta(gorm: "not null") +} + +type Tag { + id: ID! @meta(gorm: "primaryKey;not null") + createdAt: Time + updatedAt: Time + name: String! @meta(gorm: "unique;not null") + userID: ID! @meta(gorm: "not null") +} + +type Album { + id: ID! @meta(gorm: "primaryKey;not null") + createdAt: Time + updatedAt: Time + name: String! @meta(gorm: "unique;not null") + userID: ID! @meta(gorm: "not null") +} + +# ------------------------------------------------------------ +# ---------------------- Object Filters ---------------------- +# ------------------------------------------------------------ + +input UserFilter { + id: IDFilter + createdAt: TimeFilter + updatedAt: TimeFilter + username: StringFilter + firstName: StringFilter + lastName: StringFilter + role: RoleFilter + authType: AuthTypeFilter + + # and: UserFilter + # or: UserFilter +} + +input MediaItemFilter { + id: IDFilter + createdAt: TimeFilter + updatedAt: TimeFilter + exifDate: TimeFilter + latitude: FloatFilter + longitude: FloatFilter + isVideo: BooleanFilter + origName: StringFilter + tags: TagFilter + albums: AlbumFilter + + # and: MediaItemFilter + # or: MediaItemFilter +} + +input DeviceFilter { + id: IDFilter + createdAt: TimeFilter + updatedAt: TimeFilter + name: StringFilter + type: DeviceTypeFilter + + # and: MediaItemFilter + # or: MediaItemFilter +} + +input TagFilter { + id: IDFilter + createdAt: TimeFilter + updatedAt: TimeFilter + name: StringFilter + + # and: MediaItemFilter + # or: MediaItemFilter +} + +input AlbumFilter { + id: IDFilter + createdAt: TimeFilter + updatedAt: TimeFilter + name: StringFilter + + # and: MediaItemFilter + # or: MediaItemFilter +} + +# ------------------------------------------------------------ +# -------------------------- Inputs -------------------------- +# ------------------------------------------------------------ + +input NewUser { + email: String! + username: String! + firstName: String + lastName: String + role: Role! + authType: AuthType! + password: String +} + +input NewMediaItem { + file: Upload! + tags: [ID!] + albums: [ID!] +} + +input NewTag { + name: String! +} + +input NewAlbum { + name: String! +} + +input Page { + size: Int + page: Int +} + +input Order { + by: String + direction: OrderDirection +} + +# ------------------------------------------------------------ +# ------------------------ Responses ------------------------- +# ------------------------------------------------------------ + +type PageResponse { + size: Int! + page: Int! + total: Int! +} + +type MediaItemResponse { + data: [MediaItem] + page: PageResponse! +} + +type UserResponse { + data: [User] + page: PageResponse! +} + +type DeviceResponse { + data: [Device] + page: PageResponse! +} + +type TagResponse { + data: [Tag] + page: PageResponse! +} + +type AlbumResponse { + data: [Album] + page: PageResponse! +} + +# ------------------------------------------------------------ +# --------------------- Query & Mutations -------------------- +# ------------------------------------------------------------ + +type Query { + # Authentication + login( + user: String! + password: String! + deviceID: ID + ): AuthResponse! + logout: AuthResponse! @hasMinRole(role: User) + + # Single Item + mediaItem( + id: ID! + ): MediaItem! @hasMinRole(role: User) + device( + id: ID! + ): Device! @hasMinRole(role: User) + album( + id: ID! + ): Album! @hasMinRole(role: User) + user( + id: ID! + ): User! @hasMinRole(role: Admin) + tag( + id: ID! + ): Tag! @hasMinRole(role: User) + me: User! @hasMinRole(role: User) + + # All + mediaItems( + filter: MediaItemFilter + page: Page + order: Order + ): MediaItemResponse! @hasMinRole(role: User) + devices( + filter: DeviceFilter + page: Page + order: Order + ): DeviceResponse! @hasMinRole(role: User) + albums( + filter: AlbumFilter + page: Page + order: Order + ): AlbumResponse! @hasMinRole(role: User) + tags( + filter: TagFilter + page: Page + order: Order + ): TagResponse! @hasMinRole(role: User) + users( + filter: UserFilter + page: Page + order: Order + ): UserResponse! @hasMinRole(role: Admin) +} + +type Mutation { + createMediaItem(input: NewMediaItem!): MediaItem! @hasMinRole(role: User) + createAlbum(input: NewAlbum!): Album! @hasMinRole(role: User) + createTag(input: NewTag!): Tag! @hasMinRole(role: User) + createUser(input: NewUser!): User! @hasMinRole(role: Admin) +}