diff --git a/Cargo.toml b/Cargo.toml index e8c26d51..bf34aca4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bear_tracks" -version = "5.0.5" +version = "5.1.0" edition = "2021" authors = ["Jayen Agrawal"] description = "a scouting app for frc" @@ -35,7 +35,7 @@ r2d2 = "0.8" r2d2_sqlite = "0.22" rand = "0.8.5" regex = "1.10.3" -reqwest = "0.11.22" +reqwest = { version = "0.11.22", features = ["blocking"] } serde = { version = "1.0", features = ["derive"] } serde_cbor_2 = { version = "0.12.0-dev" } serde_json = "1.0" diff --git a/build.rs b/build.rs index 9887da6e..d725bbb5 100644 --- a/build.rs +++ b/build.rs @@ -3,4 +3,4 @@ use static_files::resource_dir; fn main() -> std::io::Result<()> { // include files in ./static/public in binary resource_dir("./static/public").build() -} \ No newline at end of file +} diff --git a/ios/beartracks/bearTracks.xcodeproj/project.pbxproj b/ios/beartracks/bearTracks.xcodeproj/project.pbxproj index 87711292..85ab24d2 100644 --- a/ios/beartracks/bearTracks.xcodeproj/project.pbxproj +++ b/ios/beartracks/bearTracks.xcodeproj/project.pbxproj @@ -25,8 +25,34 @@ F5AE2E5A2B5288FB0033DB0D /* URLSessionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5AE2E592B5288FB0033DB0D /* URLSessionConfiguration.swift */; }; F5AE2E5C2B52FD430033DB0D /* LoginStateValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5AE2E5B2B52FD430033DB0D /* LoginStateValidator.swift */; }; F5AE2E5E2B52FF3F0033DB0D /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5AE2E5D2B52FF3F0033DB0D /* LoginView.swift */; }; + F5B8D1AF2B9670F200D3F230 /* beartracks_watchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5B8D1AE2B9670F200D3F230 /* beartracks_watchApp.swift */; }; + F5B8D1B32B9670F200D3F230 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F5B8D1B22B9670F200D3F230 /* Assets.xcassets */; }; + F5B8D1B62B9670F200D3F230 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F5B8D1B52B9670F200D3F230 /* Preview Assets.xcassets */; }; + F5B8D1B92B9670F200D3F230 /* beartracks-watch Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = F5B8D1AC2B9670F200D3F230 /* beartracks-watch Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + F5B8D1BD2B96710900D3F230 /* URLSessionConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5AE2E592B5288FB0033DB0D /* URLSessionConfiguration.swift */; }; + F5B8D1BE2B96715500D3F230 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5081BA12B741D15001497DB /* AppState.swift */; }; + F5B8D1BF2B96716D00D3F230 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5AE2E4E2B527E170033DB0D /* SettingsManager.swift */; }; + F5B8D1C02B96719F00D3F230 /* Teams.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5AE2E4D2B527E170033DB0D /* Teams.swift */; }; + F5B8D1C12B96720900D3F230 /* LoginStateValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5AE2E5B2B52FD430033DB0D /* LoginStateValidator.swift */; }; + F5B8D1C22B96727400D3F230 /* TeamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F540A13C2B549D2500611384 /* TeamView.swift */; }; + F5B8D1C32B96727A00D3F230 /* DetailedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5AE2E402B527E050033DB0D /* DetailedView.swift */; }; + F5B8D1C42B96728500D3F230 /* TeamViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F540A13E2B549D2E00611384 /* TeamViewModel.swift */; }; + F5B8D1C62B96729200D3F230 /* TeamStatController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F59F8F7C2B7D2AD600D35A56 /* TeamStatController.swift */; }; + F5B8D1C72B9672A200D3F230 /* DataViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5AE2E442B527E050033DB0D /* DataViewModel.swift */; }; + F5B8D1C92B96779400D3F230 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5AE2E5D2B52FF3F0033DB0D /* LoginView.swift */; }; + F5BDD40A2B96CD570099C7A7 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5AE2E522B527E170033DB0D /* SettingsView.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + F5B8D1B72B9670F200D3F230 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F54E76B62B527D96003C65A2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F5B8D1AB2B9670F200D3F230; + remoteInfo = "beartracks-watch Watch App"; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ F5B95B632B58B04400AB888C /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; @@ -44,6 +70,7 @@ dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; dstSubfolderSpec = 16; files = ( + F5B8D1B92B9670F200D3F230 /* beartracks-watch Watch App.app in Embed Watch Content */, ); name = "Embed Watch Content"; runOnlyForDeploymentPostprocessing = 0; @@ -70,6 +97,12 @@ F5AE2E592B5288FB0033DB0D /* URLSessionConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionConfiguration.swift; sourceTree = ""; }; F5AE2E5B2B52FD430033DB0D /* LoginStateValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginStateValidator.swift; sourceTree = ""; }; F5AE2E5D2B52FF3F0033DB0D /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; + F5B8D1AC2B9670F200D3F230 /* beartracks-watch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "beartracks-watch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + F5B8D1AE2B9670F200D3F230 /* beartracks_watchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = beartracks_watchApp.swift; sourceTree = ""; }; + F5B8D1B22B9670F200D3F230 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + F5B8D1B52B9670F200D3F230 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + F5B8D1C82B96762D00D3F230 /* beartracks-watch Watch App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "beartracks-watch Watch App.entitlements"; sourceTree = ""; }; + F5BDD4092B96B78D0099C7A7 /* beartracks-watch-Watch-App-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "beartracks-watch-Watch-App-Info.plist"; sourceTree = SOURCE_ROOT; }; F5D3D3402B5A098D00A88A00 /* bearTracks.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bearTracks.entitlements; sourceTree = ""; }; F5D3D3452B5A17AE00A88A00 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ @@ -82,6 +115,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F5B8D1A92B9670F200D3F230 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -89,6 +129,7 @@ isa = PBXGroup; children = ( F54E76C02B527D96003C65A2 /* bearTracks */, + F5B8D1AD2B9670F200D3F230 /* beartracks-watch Watch App */, F54E76BF2B527D96003C65A2 /* Products */, ); sourceTree = ""; @@ -97,6 +138,7 @@ isa = PBXGroup; children = ( F54E76BE2B527D96003C65A2 /* bearTracks.app */, + F5B8D1AC2B9670F200D3F230 /* beartracks-watch Watch App.app */, ); name = Products; sourceTree = ""; @@ -136,6 +178,26 @@ path = "Preview Content"; sourceTree = ""; }; + F5B8D1AD2B9670F200D3F230 /* beartracks-watch Watch App */ = { + isa = PBXGroup; + children = ( + F5BDD4092B96B78D0099C7A7 /* beartracks-watch-Watch-App-Info.plist */, + F5B8D1C82B96762D00D3F230 /* beartracks-watch Watch App.entitlements */, + F5B8D1AE2B9670F200D3F230 /* beartracks_watchApp.swift */, + F5B8D1B22B9670F200D3F230 /* Assets.xcassets */, + F5B8D1B42B9670F200D3F230 /* Preview Content */, + ); + path = "beartracks-watch Watch App"; + sourceTree = ""; + }; + F5B8D1B42B9670F200D3F230 /* Preview Content */ = { + isa = PBXGroup; + children = ( + F5B8D1B52B9670F200D3F230 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -152,12 +214,30 @@ buildRules = ( ); dependencies = ( + F5B8D1B82B9670F200D3F230 /* PBXTargetDependency */, ); name = bearTracks; productName = bearTracks; productReference = F54E76BE2B527D96003C65A2 /* bearTracks.app */; productType = "com.apple.product-type.application"; }; + F5B8D1AB2B9670F200D3F230 /* beartracks-watch Watch App */ = { + isa = PBXNativeTarget; + buildConfigurationList = F5B8D1BC2B9670F200D3F230 /* Build configuration list for PBXNativeTarget "beartracks-watch Watch App" */; + buildPhases = ( + F5B8D1A82B9670F200D3F230 /* Sources */, + F5B8D1A92B9670F200D3F230 /* Frameworks */, + F5B8D1AA2B9670F200D3F230 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "beartracks-watch Watch App"; + productName = "beartracks-watch Watch App"; + productReference = F5B8D1AC2B9670F200D3F230 /* beartracks-watch Watch App.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -171,6 +251,9 @@ F54E76BD2B527D96003C65A2 = { CreatedOnToolsVersion = 15.2; }; + F5B8D1AB2B9670F200D3F230 = { + CreatedOnToolsVersion = 15.2; + }; }; }; buildConfigurationList = F54E76B92B527D96003C65A2 /* Build configuration list for PBXProject "bearTracks" */; @@ -187,6 +270,7 @@ projectRoot = ""; targets = ( F54E76BD2B527D96003C65A2 /* bearTracks */, + F5B8D1AB2B9670F200D3F230 /* beartracks-watch Watch App */, ); }; /* End PBXProject section */ @@ -201,6 +285,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F5B8D1AA2B9670F200D3F230 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F5B8D1B62B9670F200D3F230 /* Preview Assets.xcassets in Resources */, + F5B8D1B32B9670F200D3F230 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -227,8 +320,36 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F5B8D1A82B9670F200D3F230 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F5B8D1C02B96719F00D3F230 /* Teams.swift in Sources */, + F5B8D1C32B96727A00D3F230 /* DetailedView.swift in Sources */, + F5B8D1C62B96729200D3F230 /* TeamStatController.swift in Sources */, + F5B8D1C12B96720900D3F230 /* LoginStateValidator.swift in Sources */, + F5B8D1C22B96727400D3F230 /* TeamView.swift in Sources */, + F5B8D1C42B96728500D3F230 /* TeamViewModel.swift in Sources */, + F5B8D1BF2B96716D00D3F230 /* SettingsManager.swift in Sources */, + F5B8D1BD2B96710900D3F230 /* URLSessionConfiguration.swift in Sources */, + F5B8D1AF2B9670F200D3F230 /* beartracks_watchApp.swift in Sources */, + F5B8D1C92B96779400D3F230 /* LoginView.swift in Sources */, + F5B8D1BE2B96715500D3F230 /* AppState.swift in Sources */, + F5B8D1C72B9672A200D3F230 /* DataViewModel.swift in Sources */, + F5BDD40A2B96CD570099C7A7 /* SettingsView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + F5B8D1B82B9670F200D3F230 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F5B8D1AB2B9670F200D3F230 /* beartracks-watch Watch App */; + targetProxy = F5B8D1B72B9670F200D3F230 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ F54E76CA2B527D97003C65A2 /* Debug */ = { isa = XCBuildConfiguration; @@ -358,7 +479,7 @@ CODE_SIGN_ENTITLEMENTS = bearTracks/bearTracks.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 19; + CURRENT_PROJECT_VERSION = 25; DEVELOPMENT_ASSET_PATHS = "\"bearTracks/Preview Content\""; DEVELOPMENT_TEAM = D6MFYYVHA8; ENABLE_PREVIEWS = YES; @@ -378,7 +499,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.0.501; + MARKETING_VERSION = 5.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.jayagra.beartracks; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = xros; @@ -402,7 +523,7 @@ CODE_SIGN_ENTITLEMENTS = bearTracks/bearTracks.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 19; + CURRENT_PROJECT_VERSION = 25; DEVELOPMENT_ASSET_PATHS = "\"bearTracks/Preview Content\""; DEVELOPMENT_TEAM = D6MFYYVHA8; ENABLE_PREVIEWS = YES; @@ -422,7 +543,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.0.501; + MARKETING_VERSION = 5.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.jayagra.beartracks; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = xros; @@ -437,6 +558,70 @@ }; name = Release; }; + F5B8D1BA2B9670F200D3F230 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "beartracks-watch Watch App/beartracks-watch Watch App.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"beartracks-watch Watch App/Preview Content\""; + DEVELOPMENT_TEAM = D6MFYYVHA8; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "beartracks-watch-Watch-App-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "beartracks-watch"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.jayagra.beartracks; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jayagra.beartracks.watchkitapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 9.0; + }; + name = Debug; + }; + F5B8D1BB2B9670F200D3F230 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "beartracks-watch Watch App/beartracks-watch Watch App.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"beartracks-watch Watch App/Preview Content\""; + DEVELOPMENT_TEAM = D6MFYYVHA8; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "beartracks-watch-Watch-App-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "beartracks-watch"; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.jayagra.beartracks; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jayagra.beartracks.watchkitapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 9.0; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -458,6 +643,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + F5B8D1BC2B9670F200D3F230 /* Build configuration list for PBXNativeTarget "beartracks-watch Watch App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F5B8D1BA2B9670F200D3F230 /* Debug */, + F5B8D1BB2B9670F200D3F230 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = F54E76B62B527D96003C65A2 /* Project object */; diff --git a/ios/beartracks/bearTracks/AppState.swift b/ios/beartracks/bearTracks/AppState.swift index a1c20c45..e942e6ea 100644 --- a/ios/beartracks/bearTracks/AppState.swift +++ b/ios/beartracks/bearTracks/AppState.swift @@ -11,16 +11,19 @@ import Combine class AppState: ObservableObject { #if targetEnvironment(macCatalyst) @Published public var selectedTab: Tab? = .teams +#elseif os(watchOS) #else @Published public var selectedTab: Tab = .teams #endif @Published public var loginRequired: Bool = false private var cancellables: Set = [] +#if !os(watchOS) init() { $selectedTab .receive(on: DispatchQueue.main) .sink { _ in } .store(in: &cancellables) } +#endif } diff --git a/ios/beartracks/bearTracks/Assets.xcassets/AccentColor.colorset/Contents.json b/ios/beartracks/bearTracks/Assets.xcassets/AccentColor.colorset/Contents.json index eb878970..04829b2f 100644 --- a/ios/beartracks/bearTracks/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/ios/beartracks/bearTracks/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,15 @@ { "colors" : [ { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x96", + "green" : "0x93", + "red" : "0x0A" + } + }, "idiom" : "universal" } ], diff --git a/ios/beartracks/bearTracks/DataViewModel.swift b/ios/beartracks/bearTracks/DataViewModel.swift index cde0824b..083945d8 100644 --- a/ios/beartracks/bearTracks/DataViewModel.swift +++ b/ios/beartracks/bearTracks/DataViewModel.swift @@ -19,7 +19,7 @@ class DataViewModel: ObservableObject { } func fetchEventJson(completionBlock: @escaping ([DataEntry]) -> Void) -> Void { - guard let url = URL(string: "https://beartracks.io/api/v1/data/brief/event/\(UserDefaults.standard.string(forKey: "season") ?? "2024")/\(UserDefaults.standard.string(forKey: "eventCode") ?? "CAFR")") else { + guard let url = URL(string: "https://beartracks.io/api/v1/data/brief/event/\(UserDefaults(suiteName: "group.com.jayagra.beartracks")?.string(forKey: "season") ?? "2024")/\(UserDefaults(suiteName: "group.com.jayagra.beartracks")?.string(forKey: "eventCode") ?? "CAFR")") else { return } diff --git a/ios/beartracks/bearTracks/DetailedView.swift b/ios/beartracks/bearTracks/DetailedView.swift index efadb369..845aba93 100644 --- a/ios/beartracks/bearTracks/DetailedView.swift +++ b/ios/beartracks/bearTracks/DetailedView.swift @@ -24,11 +24,19 @@ struct DetailedView: View { if detailData[0].FullMain.season == 2024 { ScrollView { Text("Team \(String(detailData[0].FullMain.team))") +#if !os(watchOS) .font(.largeTitle) +#else + .font(.title3) +#endif .padding([.top, .leading]) .frame(maxWidth: .infinity, alignment: .leading) Text("\(detailData[0].FullMain.level) \(String(detailData[0].FullMain.match_num)) @ \(detailData[0].FullMain.event) \(String(detailData[0].FullMain.season))") +#if !os(watchOS) .font(.title2) +#else + .font(.body) +#endif .padding(.leading) .frame(maxWidth: .infinity, alignment: .leading) VStack { @@ -43,19 +51,31 @@ struct DetailedView: View { HStack { VStack { Text("\(String(detailData[0].FullMain.analysis.split(separator: ",")[3]))s") +#if !os(watchOS) .font(.title) +#else + .font(.title3) +#endif Text("intake") } .frame(maxWidth: .infinity) VStack { Text("\(String(detailData[0].FullMain.analysis.split(separator: ",")[4]))s") +#if !os(watchOS) .font(.title) +#else + .font(.title3) +#endif Text("travel") } .frame(maxWidth: .infinity) VStack { Text("\(String(detailData[0].FullMain.analysis.split(separator: ",")[5]))s") +#if !os(watchOS) .font(.title) +#else + .font(.title3) +#endif Text("outtake") } .frame(maxWidth: .infinity) @@ -67,13 +87,21 @@ struct DetailedView: View { HStack { VStack { Text(String(detailData[0].FullMain.analysis.split(separator: ",")[6])) +#if !os(watchOS) .font(.title) +#else + .font(.title3) +#endif Text("speaker") } .frame(maxWidth: .infinity) VStack { Text(String(detailData[0].FullMain.analysis.split(separator: ",")[7])) +#if !os(watchOS) .font(.title) +#else + .font(.title3) +#endif Text("amplifier") } .frame(maxWidth: .infinity) @@ -87,7 +115,7 @@ struct DetailedView: View { .frame(maxWidth: .infinity, alignment: .leading) VStack { Divider() - ForEach(gameData, id: \.id) { matchTime in + ForEach(gameData) { matchTime in VStack { HStack { switch matchTime.score_type { @@ -159,10 +187,19 @@ struct DetailedView: View { HStack { Spacer() Label(String(format: "%.1f", matchTime.intake), systemImage: "tray.and.arrow.down") +#if os(watchOS) + .labelStyle(.titleOnly) +#endif Spacer() Label(String(format: "%.1f", matchTime.travel), systemImage: "arrow.up.and.down.and.arrow.left.and.right") +#if os(watchOS) + .labelStyle(.titleOnly) +#endif Spacer() Label(String(format: "%.1f", matchTime.outtake), systemImage: "tray.and.arrow.up") +#if os(watchOS) + .labelStyle(.titleOnly) +#endif Spacer() } .padding(.top) @@ -294,10 +331,17 @@ struct FullMainData: Codable { } /// 2024 season specific data structure for deocding the `game` key of `FullMainData` -struct MatchTime2024: Codable { - let id: Int +struct MatchTime2024: Codable, Identifiable { + var id = UUID() let score_type: Int let intake: Float let outtake: Float let travel: Float + + enum CodingKeys: CodingKey { + case score_type + case intake + case outtake + case travel + } } diff --git a/ios/beartracks/bearTracks/LoginView.swift b/ios/beartracks/bearTracks/LoginView.swift index 1e6a1177..0707ddfb 100644 --- a/ios/beartracks/bearTracks/LoginView.swift +++ b/ios/beartracks/bearTracks/LoginView.swift @@ -18,36 +18,49 @@ struct LoginView: View { var body: some View { VStack { +#if !os(watchOS) Text("bearTracks") .font(.title) - Text("v5.0.5 • 2024") + Text("v5.1.0 • 2024") +#endif if !loading { if !create { +#if !os(watchOS) Text("log in") .font(.title3) .padding(.top) +#endif TextField("username", text: $authData[0]) +#if !os(watchOS) .padding([.leading, .trailing, .bottom]) .textFieldStyle(RoundedBorderTextFieldStyle()) +#endif .autocorrectionDisabled(true) .textInputAutocapitalization(.never) .textContentType(.username) SecureField("password", text: $authData[1]) +#if !os(watchOS) .padding() .textFieldStyle(RoundedBorderTextFieldStyle()) +#endif .autocorrectionDisabled(true) .textInputAutocapitalization(.never) .textContentType(.password) Button("login") { authAction(type: "login", data: ["username": authData[0], "password": authData[1]]) } +#if !os(watchOS) .padding() +#endif .font(.title3) .buttonStyle(.bordered) +#if !os(watchOS) Button("create") { self.create = true } +#endif } else { +#if !os(watchOS) Text("create account") .font(.title3) .padding(.top) @@ -83,6 +96,7 @@ struct LoginView: View { Button("login") { self.create = false } +#endif } } else { Spacer() diff --git a/ios/beartracks/bearTracks/MatchList.swift b/ios/beartracks/bearTracks/MatchList.swift index bc30db98..edad9684 100644 --- a/ios/beartracks/bearTracks/MatchList.swift +++ b/ios/beartracks/bearTracks/MatchList.swift @@ -139,7 +139,7 @@ struct MatchList: View { } func fetchMatchJson() { - guard let url = URL(string: "https://beartracks.io/api/v1/events/matches/\(UserDefaults.standard.string(forKey: "season") ?? "2024")/\(UserDefaults.standard.string(forKey: "eventCode") ?? "CAFR")/qualification/\( UserDefaults.standard.string(forKey: "teamNumber") ?? "766")") else { + guard let url = URL(string: "https://beartracks.io/api/v1/events/matches/\(UserDefaults(suiteName: "group.com.jayagra.beartracks")?.string(forKey: "season") ?? "2024")/\(UserDefaults(suiteName: "group.com.jayagra.beartracks")?.string(forKey: "eventCode") ?? "CAFR")/qualification/\(UserDefaults(suiteName: "group.com.jayagra.beartracks")?.string(forKey: "teamNumber") ?? "766")") else { return } diff --git a/ios/beartracks/bearTracks/SettingsManager.swift b/ios/beartracks/bearTracks/SettingsManager.swift index 830b1485..eac090ba 100644 --- a/ios/beartracks/bearTracks/SettingsManager.swift +++ b/ios/beartracks/bearTracks/SettingsManager.swift @@ -18,6 +18,6 @@ class SettingsManager { "season": "2024", "darkMode": true, ] - UserDefaults.standard.register(defaults: defaults) + UserDefaults(suiteName: "group.com.jayagra.beartracks")?.register(defaults: defaults) } } diff --git a/ios/beartracks/bearTracks/SettingsView.swift b/ios/beartracks/bearTracks/SettingsView.swift index 0b8e2082..b6afafbe 100644 --- a/ios/beartracks/bearTracks/SettingsView.swift +++ b/ios/beartracks/bearTracks/SettingsView.swift @@ -9,10 +9,10 @@ import SwiftUI /// Settings window for event config & logging out struct SettingsView: View { - @State private var teamNumberInput: String = UserDefaults.standard.string(forKey: "teamNumber") ?? "" - @State private var eventCodeInput: String = UserDefaults.standard.string(forKey: "eventCode") ?? "" - @State private var seasonInput: String = UserDefaults.standard.string(forKey: "season") ?? "" - @State private var darkMode: Bool = UserDefaults.standard.bool(forKey: "darkMode") + @State private var teamNumberInput: String = UserDefaults(suiteName: "group.com.jayagra.beartracks")?.string(forKey: "teamNumber") ?? "" + @State private var eventCodeInput: String = UserDefaults(suiteName: "group.com.jayagra.beartracks")?.string(forKey: "eventCode") ?? "" + @State private var seasonInput: String = UserDefaults(suiteName: "group.com.jayagra.beartracks")?.string(forKey: "season") ?? "" + @State private var darkMode: Bool = UserDefaults(suiteName: "group.com.jayagra.beartracks")?.bool(forKey: "darkMode") ?? true @State private var showAlert = false @State private var settingsOptions: [DataMetadata] = [] @State private var showConfirm = false @@ -35,9 +35,11 @@ struct SettingsView: View { .tag(teamNumberInput) } } +#if !os(watchOS) .pickerStyle(.menu) +#endif .onChange(of: teamNumberInput) { value in - UserDefaults.standard.set(teamNumberInput, forKey: "teamNumber") + UserDefaults(suiteName: "group.com.jayagra.beartracks")?.set(teamNumberInput, forKey: "teamNumber") } Picker("Event Code", selection: $eventCodeInput) { if !settingsOptions.isEmpty { @@ -50,9 +52,11 @@ struct SettingsView: View { .tag(eventCodeInput) } } +#if !os(watchOS) .pickerStyle(.menu) +#endif .onChange(of: eventCodeInput) { value in - UserDefaults.standard.set(eventCodeInput, forKey: "eventCode") + UserDefaults(suiteName: "group.com.jayagra.beartracks")?.set(eventCodeInput, forKey: "eventCode") } Picker("Season", selection: $seasonInput) { if !settingsOptions.isEmpty { @@ -65,13 +69,15 @@ struct SettingsView: View { .tag(seasonInput) } } +#if !os(watchOS) .pickerStyle(.menu) +#endif .onChange(of: seasonInput) { value in - UserDefaults.standard.set(seasonInput, forKey: "season") + UserDefaults(suiteName: "group.com.jayagra.beartracks")?.set(seasonInput, forKey: "season") } Toggle("Dark Mode", isOn: $darkMode) .onChange(of: darkMode) { value in - UserDefaults.standard.set(darkMode, forKey: "darkMode") + UserDefaults(suiteName: "group.com.jayagra.beartracks")?.set(darkMode, forKey: "darkMode") showAlert = true } } @@ -126,6 +132,9 @@ struct SettingsView: View { self.settingsOptions = result } } +#if os(watchOS) + .ignoresSafeArea(edges: .bottom) +#endif } func loadSettingsJson(completionBlock: @escaping ([DataMetadata]) -> Void) -> Void { @@ -170,23 +179,23 @@ struct SettingsView: View { if data != nil { if let httpResponse = response as? HTTPURLResponse { if httpResponse.statusCode == 200 { -#if !os(visionOS) +#if !os(visionOS) && !os(watchOS) UINotificationFeedbackGenerator().notificationOccurred(.success) #endif } else { -#if !os(visionOS) +#if !os(visionOS) && !os(watchOS) UINotificationFeedbackGenerator().notificationOccurred(.error) #endif } } } else { -#if !os(visionOS) +#if !os(visionOS) && !os(watchOS) UINotificationFeedbackGenerator().notificationOccurred(.error) #endif } }.resume() } catch { -#if !os(visionOS) +#if !os(visionOS) && !os(watchOS) UINotificationFeedbackGenerator().notificationOccurred(.error) #endif } diff --git a/ios/beartracks/bearTracks/TeamStatController.swift b/ios/beartracks/bearTracks/TeamStatController.swift index 079e4821..8032d0db 100644 --- a/ios/beartracks/bearTracks/TeamStatController.swift +++ b/ios/beartracks/bearTracks/TeamStatController.swift @@ -17,7 +17,7 @@ class TeamStatController: ObservableObject { @Published public var teamData: [TeamStats] = [] public func fetchTeamDataJson() { - guard let url = URL(string: "https://beartracks.io/api/v1/game/team_data/2024/\(UserDefaults.standard.string(forKey: "eventCode") ?? "CAFR")/\(teamNumber)") else { return } + guard let url = URL(string: "https://beartracks.io/api/v1/game/team_data/2024/\(UserDefaults(suiteName: "group.com.jayagra.beartracks")?.string(forKey: "eventCode") ?? "CAFR")/\(teamNumber)") else { return } sharedSession.dataTask(with: url) { data, _, error in if let data = data { do { diff --git a/ios/beartracks/bearTracks/TeamView.swift b/ios/beartracks/bearTracks/TeamView.swift index e7f74db8..d7c49fa2 100644 --- a/ios/beartracks/bearTracks/TeamView.swift +++ b/ios/beartracks/bearTracks/TeamView.swift @@ -20,7 +20,8 @@ struct TeamView: View { var body: some View { VStack { - if !(dataItems.teamData.isEmpty || dataItems.teamMatches.isEmpty) { + if !dataItems.teamMatches.isEmpty { +#if !os(watchOS) HStack { VStack { Text(String(dataItems.teamData.first?.wins ?? 0)) @@ -78,12 +79,17 @@ struct TeamView: View { } .padding([.leading, .trailing]) Divider() +#endif List { ForEach(dataItems.teamMatches) { entry in VStack { HStack { Text("match \(String(entry.Brief.match_num))") +#if !os(watchOS) .font(.title) +#else + .font(.title3) +#endif .padding(.leading) .frame(maxWidth: .infinity, alignment: .leading) } @@ -112,7 +118,11 @@ struct TeamView: View { .navigationTitle("#\(dataItems.getSelectedItem())") } } else { - Text("loading team...") + if dataItems.loadComplete.0 && dataItems.loadComplete.1 && (dataItems.teamData.isEmpty || dataItems.teamMatches.isEmpty) { + Label("loading failed", systemImage: "xmark.seal.fill") + } else { + Text("loading team...") + } } } .padding() diff --git a/ios/beartracks/bearTracks/TeamViewModel.swift b/ios/beartracks/bearTracks/TeamViewModel.swift index 6cd4a51e..2ed9feae 100644 --- a/ios/beartracks/bearTracks/TeamViewModel.swift +++ b/ios/beartracks/bearTracks/TeamViewModel.swift @@ -21,6 +21,7 @@ class TeamViewModel: ObservableObject { @Published private(set) var selectedItem: String = "-1" @State private var isShowingSheet = false @Published public var targetTeam: String = "-1" + @Published public var loadComplete: (Bool, Bool) = (false, false) init(team: String) { self.targetTeam = team @@ -38,7 +39,8 @@ class TeamViewModel: ObservableObject { } func fetchTeamMatchesJson(completionBlock: @escaping ([DataEntry]) -> Void) -> Void { - guard let url = URL(string: "https://beartracks.io/api/v1/data/brief/team/\(UserDefaults.standard.string(forKey: "season") ?? "2024")/\(UserDefaults.standard.string(forKey: "eventCode") ?? "CAFR")/\(targetTeam)") else { + self.loadComplete.0 = false; + guard let url = URL(string: "https://beartracks.io/api/v1/data/brief/team/\(UserDefaults(suiteName: "group.com.jayagra.beartracks")?.string(forKey: "season") ?? "2024")/\(UserDefaults(suiteName: "group.com.jayagra.beartracks")?.string(forKey: "eventCode") ?? "CAFR")/\(targetTeam)") else { return } @@ -48,6 +50,7 @@ class TeamViewModel: ObservableObject { let requestTask = sharedSession.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in + self.loadComplete.0 = true; if let data = data { do { let decoder = JSONDecoder() @@ -74,7 +77,8 @@ class TeamViewModel: ObservableObject { } func fetchStatboticsTeamJson(completionBlock: @escaping (StatboticsTeamData) -> Void) -> Void { - guard let url = URL(string: "https://api.statbotics.io/v2/team_event/\(targetTeam)/\(UserDefaults.standard.string(forKey: "season") ?? "2024")\(UserDefaults.standard.string(forKey: "eventCode")?.lowercased() ?? "cafr")") else { + self.loadComplete.1 = false; + guard let url = URL(string: "https://api.statbotics.io/v2/team_event/\(targetTeam)/\(UserDefaults(suiteName: "group.com.jayagra.beartracks")?.string(forKey: "season") ?? "2024")\(UserDefaults(suiteName: "group.com.jayagra.beartracks")?.string(forKey: "eventCode")?.lowercased() ?? "cafr")") else { return } @@ -84,6 +88,7 @@ class TeamViewModel: ObservableObject { let requestTask = sharedSession.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in + self.loadComplete.1 = true; if let data = data { do { let decoder = JSONDecoder() @@ -107,9 +112,13 @@ class TeamViewModel: ObservableObject { self.fetchTeamMatchesJson() { (output) in self.teamMatches = output } +#if !os(watchOS) self.fetchStatboticsTeamJson() { (output) in self.teamData = [output] } +#else + self.loadComplete.1 = true +#endif } } diff --git a/ios/beartracks/bearTracks/Teams.swift b/ios/beartracks/bearTracks/Teams.swift index b79a94c0..94026a8b 100644 --- a/ios/beartracks/bearTracks/Teams.swift +++ b/ios/beartracks/bearTracks/Teams.swift @@ -24,11 +24,19 @@ struct Teams: View { VStack { HStack { Text("\(String(index + 1))") +#if !os(watchOS) .font(.title) +#else + .font(.title3) +#endif .padding(.leading) .frame(maxWidth: .infinity, alignment: .leading) Text("\(String(team.team.team))") +#if !os(watchOS) .font(.title) +#else + .font(.title3) +#endif .padding(.trailing) .frame(maxWidth: .infinity, alignment: .trailing) } @@ -47,12 +55,28 @@ struct Teams: View { #if targetEnvironment(macCatalyst) .padding([.top, .bottom]) #endif -#if !os(visionOS) +#if os(visionOS) .padding(.bottom) #endif } .contentShape(Rectangle()) } +#if os(watchOS) + Section { + VStack { + NavigationLink(destination: SettingsView()) { + HStack { + Text("Settings") + Spacer() + Label("", systemImage: "chevron.forward") + .labelStyle(.iconOnly) + } + .padding([.leading, .trailing]) + } + } + .padding([.leading, .trailing]) + } +#endif } .navigationTitle("Teams") .navigationDestination(isPresented: $loadState.0) { @@ -101,7 +125,7 @@ struct Teams: View { } func fetchTeamsJson() { - guard let url = URL(string: "https://beartracks.io/api/v1/data/teams/\(UserDefaults.standard.string(forKey: "season") ?? "2024")/\(UserDefaults.standard.string(forKey: "eventCode") ?? "CAFR")") else { + guard let url = URL(string: "https://beartracks.io/api/v1/data/teams/\(UserDefaults(suiteName: "group.com.jayagra.beartracks")?.string(forKey: "season") ?? "2024")/\(UserDefaults(suiteName: "group.com.jayagra.beartracks")?.string(forKey: "eventCode") ?? "CAFR")") else { return } diff --git a/ios/beartracks/bearTracks/bearTracks.entitlements b/ios/beartracks/bearTracks/bearTracks.entitlements index d2d71196..4e299caf 100644 --- a/ios/beartracks/bearTracks/bearTracks.entitlements +++ b/ios/beartracks/bearTracks/bearTracks.entitlements @@ -8,6 +8,10 @@ com.apple.security.app-sandbox + com.apple.security.application-groups + + group.com.jayagra.beartracks + com.apple.security.network.client diff --git a/ios/beartracks/bearTracks/beartracksApp.swift b/ios/beartracks/bearTracks/beartracksApp.swift index 277dfc9b..aee2ae4f 100644 --- a/ios/beartracks/bearTracks/beartracksApp.swift +++ b/ios/beartracks/bearTracks/beartracksApp.swift @@ -14,7 +14,7 @@ public enum Tab { @main struct beartracksApp: App { let settingsManager = SettingsManager.shared - var darkMode: Bool = UserDefaults.standard.bool(forKey: "darkMode") + var darkMode: Bool = UserDefaults(suiteName: "group.com.jayagra.beartracks")?.bool(forKey: "darkMode") ?? true @StateObject private var appState = AppState() var body: some Scene { diff --git a/ios/beartracks/beartracks-scout.xcodeproj/project.pbxproj b/ios/beartracks/beartracks-scout.xcodeproj/project.pbxproj index 5b6e4fea..ce2cf4f1 100644 --- a/ios/beartracks/beartracks-scout.xcodeproj/project.pbxproj +++ b/ios/beartracks/beartracks-scout.xcodeproj/project.pbxproj @@ -313,7 +313,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "beartracks-scout/beartracks-scout.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 20; DEVELOPMENT_ASSET_PATHS = "\"beartracks-scout/Preview Content\""; DEVELOPMENT_TEAM = D6MFYYVHA8; ENABLE_PREVIEWS = YES; @@ -331,7 +331,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.0.5; + MARKETING_VERSION = 5.1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.jayagra.beartracks-scout"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -347,7 +347,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "beartracks-scout/beartracks-scout.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 20; DEVELOPMENT_ASSET_PATHS = "\"beartracks-scout/Preview Content\""; DEVELOPMENT_TEAM = D6MFYYVHA8; ENABLE_PREVIEWS = YES; @@ -365,7 +365,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.0.5; + MARKETING_VERSION = 5.1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.jayagra.beartracks-scout"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/ios/beartracks/beartracks-scout/Assets.xcassets/AccentColor.colorset/Contents.json b/ios/beartracks/beartracks-scout/Assets.xcassets/AccentColor.colorset/Contents.json index eb878970..ebb7638b 100644 --- a/ios/beartracks/beartracks-scout/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/ios/beartracks/beartracks-scout/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,15 @@ { "colors" : [ { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x50", + "green" : "0x96", + "red" : "0x0A" + } + }, "idiom" : "universal" } ], diff --git a/ios/beartracks/beartracks-scout/EndView.swift b/ios/beartracks/beartracks-scout/EndView.swift index f9037993..c1d0fd85 100644 --- a/ios/beartracks/beartracks-scout/EndView.swift +++ b/ios/beartracks/beartracks-scout/EndView.swift @@ -9,11 +9,16 @@ import SwiftUI struct EndView: View { @EnvironmentObject var controller: ScoutingController + @FocusState private var activeBox: ActiveBox? + + enum ActiveBox: Hashable { + case defense, driving, overall + } var body: some View { NavigationStack { VStack { - if controller.getTeamNumber() != "--" || controller.getMatchNumber() != "--" { + if controller.getTeamNumber() != "--" && controller.getMatchNumber() != 0 { Text("match \(controller.getMatchNumber()) • team \(controller.getTeamNumber())") .padding(.leading) .frame(maxWidth: .infinity, alignment: .leading) @@ -42,6 +47,8 @@ struct EndView: View { ) .frame(height: 150) .padding([.leading, .trailing]) + .focused($activeBox, equals: .defense) + .onTapGesture { activeBox = .defense } } VStack { Text("How was the driving? Did the driver seem confident?") @@ -54,6 +61,8 @@ struct EndView: View { ) .frame(height: 150) .padding([.leading, .trailing]) + .focused($activeBox, equals: .driving) + .onTapGesture { activeBox = .driving } } VStack { Text("Provide an overall description of the team's performance in this match") @@ -66,6 +75,8 @@ struct EndView: View { ) .frame(height: 150) .padding([.leading, .trailing]) + .focused($activeBox, equals: .overall) + .onTapGesture { activeBox = .overall } } } .padding(.bottom) @@ -75,6 +86,9 @@ struct EndView: View { .padding() .buttonStyle(.bordered) } + .onTapGesture { + activeBox = nil + } Spacer() } else { Text("Please select a match number and event code on the start tab.") diff --git a/ios/beartracks/beartracks-scout/GameView.swift b/ios/beartracks/beartracks-scout/GameView.swift index 5a0f0b1e..3182f654 100644 --- a/ios/beartracks/beartracks-scout/GameView.swift +++ b/ios/beartracks/beartracks-scout/GameView.swift @@ -21,7 +21,7 @@ struct GameView: View { var body: some View { NavigationStack { - if controller.getTeamNumber() != "--" && controller.getMatchNumber() != "--" { + if controller.getTeamNumber() != "--" && controller.getMatchNumber() != 0 { VStack { GeometryReader { geometry in VStack { @@ -81,21 +81,43 @@ struct GameView: View { ) .modifier(PressModifier(onPress: { self.actionState = .travel }, onRelease: { if self.ballOffset.height >= geometry.size.height * 0.2 { - let boomThing = UIImpactFeedbackGenerator(style: .medium) - boomThing.prepare(); boomThing.impactOccurred(); - if self.ballOffset.height >= geometry.size.height * 0.475 { + UINotificationFeedbackGenerator().notificationOccurred(.success) + controller.clearSpeaker() + self.holdLengths = (0, 0, 0) + } else if self.ballOffset.height <= geometry.size.height * -0.2 { + if abs(self.ballOffset.height) >= geometry.size.height * 0.475 { + UINotificationFeedbackGenerator().notificationOccurred(.error) controller.clearShuttle() } else { + UINotificationFeedbackGenerator().notificationOccurred(.warning) controller.clearAmplifier() } self.holdLengths = (0, 0, 0) - } else if self.ballOffset.height <= geometry.size.height * -0.2 { - UINotificationFeedbackGenerator().notificationOccurred(.success) - controller.clearSpeaker() - self.holdLengths = (0, 0, 0) } self.actionState = .neutral })) + if controller.matchTimes.count == 0 { + VStack { + Spacer() + Text("↑ shuttle / other ↑") + Text("amplifier") + Spacer() + Spacer() + Spacer() + HStack { + Spacer() + Text("intake") + Spacer() + Text("travel") + Spacer() + Text("outtake") + Spacer() + } + Spacer() + Text("speaker") + Spacer() + } + } } .padding(.bottom) .onChange(of: actionState) { value in @@ -157,9 +179,9 @@ struct GameView: View { private func updateBallOffset(dragValue: DragGesture.Value, totalWidth: CGFloat, totalHeight: CGFloat) { if abs(self.ballOffset.height) > totalHeight * 0.2 && abs(dragValue.translation.height) <= totalHeight * 0.2 { - UIImpactFeedbackGenerator(style: .light).impactOccurred() - } else if abs(self.ballOffset.height) <= totalHeight * 0.2 && abs(dragValue.translation.height) > totalHeight * 0.2 { UIImpactFeedbackGenerator(style: .rigid).impactOccurred() + } else if abs(self.ballOffset.height) <= totalHeight * 0.2 && abs(dragValue.translation.height) > totalHeight * 0.2 { + UIImpactFeedbackGenerator(style: .light).impactOccurred() } let newOffset = CGSize( @@ -173,24 +195,24 @@ struct GameView: View { private func getUIImage(position: ActionState, height: CGFloat) -> Image { if abs(self.ballOffset.height) >= height * 0.2 { if self.ballOffset.height < 0 { - return Image(systemName: "speaker.wave.2") - } else { if abs(self.ballOffset.height) >= height * 0.475 { return Image(systemName: "airplane") } else { return Image(systemName: "speaker.plus") } + } else { + return Image(systemName: "speaker.wave.2") } } else { switch position { case .neutral: return Image(systemName: "arrow.left.and.right") case .intake: - return Image(systemName: "chevron.down") + return Image(systemName: "tray.and.arrow.down.fill") case .travel: return Image(systemName: "arrow.up.and.down.and.arrow.left.and.right") case .outtake: - return Image(systemName: "chevron.up") + return Image(systemName: "paperplane.fill") } } } @@ -198,13 +220,13 @@ struct GameView: View { private func getBallColor(position: ActionState, height: CGFloat) -> Color { if abs(self.ballOffset.height) >= height * 0.2 { if self.ballOffset.height < 0 { - return Color.init(red: 184/255, green: 187/255, blue: 38/255) - } else { if abs(self.ballOffset.height) >= height * 0.475 { return Color.init(red: 254/255, green: 128/255, blue: 25/255) } else { return Color.init(red: 250/255, green: 189/255, blue: 47/255) } + } else { + return Color.init(red: 184/255, green: 187/255, blue: 38/255) } } else { switch position { diff --git a/ios/beartracks/beartracks-scout/LoginView.swift b/ios/beartracks/beartracks-scout/LoginView.swift index 6b0998d5..8b20f554 100644 --- a/ios/beartracks/beartracks-scout/LoginView.swift +++ b/ios/beartracks/beartracks-scout/LoginView.swift @@ -19,7 +19,7 @@ struct LoginView: View { VStack { Text("bearTracks") .font(.title) - Text("v5.0.5 • 2024") + Text("v5.1.0 • 2024") if !loading { if !create { Text("log in") diff --git a/ios/beartracks/beartracks-scout/ReviewView.swift b/ios/beartracks/beartracks-scout/ReviewView.swift index 6a3ddbd4..aa90febf 100644 --- a/ios/beartracks/beartracks-scout/ReviewView.swift +++ b/ios/beartracks/beartracks-scout/ReviewView.swift @@ -17,7 +17,7 @@ struct ReviewView: View { VStack { NavigationStack { VStack { - if controller.getTeamNumber() == "--" || controller.getMatchNumber() == "--" { + if controller.getTeamNumber() == "--" || controller.getMatchNumber() == 0 { Text("Please select a match number and event code on the start tab.") .padding() } else if controller.getDefenseResponse() == "" || controller.getDrivingResponse() == "" || controller.getOverallResponse() == "" { @@ -35,36 +35,29 @@ struct ReviewView: View { .frame(maxWidth: .infinity, alignment: .leading) VStack { Divider() - ForEach(controller.getMatchTimes(), id: \.travel) { matchTime in + ForEach($controller.matchTimes) { $matchTime in VStack { HStack { - if matchTime.score_type == 0 { + Picker("Type", selection: $matchTime.score_type) { Text("Speaker") - .font(.title3) - Spacer() - Text(String(format: "%.1f", matchTime.intake + matchTime.travel + matchTime.outtake)) - .font(.title3) - } else if matchTime.score_type == 1 { + .tag(0) Text("Amplifier") - .font(.title3) - Spacer() - Text(String(format: "%.1f", matchTime.intake + matchTime.travel + matchTime.outtake)) - .font(.title3) - } else if matchTime.score_type == 9 { - Text("Shuttle") - .font(.title3) - Spacer() - Text(String(format: "%.1f", matchTime.intake + matchTime.travel + matchTime.outtake)) - .font(.title3) + .tag(1) + Text("Shuttle/Other") + .tag(9) } + .pickerStyle(.menu) + Spacer() + Text(String(format: "%.1f", matchTime.intake + matchTime.travel + matchTime.outtake)) + .font(.title3) } HStack { Spacer() - Text(String(format: "%.1f", matchTime.intake)) + Label(String(format: "%.1f", matchTime.intake), systemImage: "tray.and.arrow.down.fill") Spacer() - Text(String(format: "%.1f", matchTime.travel)) + Label(String(format: "%.1f", matchTime.travel), systemImage: "arrow.up.and.down.and.arrow.left.and.right") Spacer() - Text(String(format: "%.1f", matchTime.outtake)) + Label(String(format: "%.1f", matchTime.outtake), systemImage: "paperplane.fill") Spacer() } } @@ -104,16 +97,16 @@ struct ReviewView: View { } .padding() } - } - Button("submit") { - showSheet = true - controller.submitData { result in - self.submitSheetState = result.0 - self.submitError = result.1 + Button("submit") { + showSheet = true + controller.submitData { result in + self.submitSheetState = result.0 + self.submitError = result.1 + } } + .padding() + .buttonStyle(.borderedProminent) } - .padding() - .buttonStyle(.borderedProminent) } } .sheet(isPresented: $showSheet) { diff --git a/ios/beartracks/beartracks-scout/ScoutingController.swift b/ios/beartracks/beartracks-scout/ScoutingController.swift index 921c71b2..ac9dea9c 100644 --- a/ios/beartracks/beartracks-scout/ScoutingController.swift +++ b/ios/beartracks/beartracks-scout/ScoutingController.swift @@ -15,13 +15,13 @@ class ScoutingController: ObservableObject { @Published public var currentTab: Tab = .start // basic meta private var eventCode: String = UserDefaults.standard.string(forKey: "eventCode") ?? "CAFR" - @Published public var matchNumber: String = "--" + @Published public var matchNumber: Int = 0 @Published public var teamNumber: String = "--" @Published public var matchList: [MatchData] = [] // match buttons @Published public var times: [Double] = [0, 0, 0] // match time store - private var matchTimes: [MatchTime] = [] + @Published public var matchTimes: [MatchTime] = [] // qualitative data store @Published public var defense: String = "" @Published public var driving: String = "" @@ -34,7 +34,7 @@ class ScoutingController: ObservableObject { private var submitSheetDetails: String = "" // getters - func getMatchNumber() -> String { return self.matchNumber } + func getMatchNumber() -> Int { return self.matchNumber } func getTeamNumber() -> String { return self.teamNumber } func getDefenseResponse() -> String { return self.defense } func getDrivingResponse() -> String { return self.driving } @@ -44,7 +44,7 @@ class ScoutingController: ObservableObject { func getSubmitSheetDetails() -> String { return self.submitSheetDetails } // setters - func setMatchNumber(match: String) { self.matchNumber = match } + func setMatchNumber(match: Int) { self.matchNumber = match } func setTeamNumber(team: String) { self.teamNumber = team } func setDefenseResponse(response: String) { self.defense = response } func setDrivingResponse(response: String) { self.driving = response } @@ -93,7 +93,7 @@ class ScoutingController: ObservableObject { } catch { encodedMatchTimes = "" } - let matchData = ScoutingDataExport(season: 2024, event: UserDefaults.standard.string(forKey: "eventCode") ?? "CAFR", match_num: Int(matchNumber) ?? 0, level: "Qualification", team: Int(teamNumber) ?? 0, game: encodedMatchTimes, defend: defense, driving: driving, overall: overall) + let matchData = ScoutingDataExport(season: 2024, event: UserDefaults.standard.string(forKey: "eventCode") ?? "CAFR", match_num: matchNumber, level: "Qualification", team: Int(teamNumber) ?? 0, game: encodedMatchTimes, defend: defense, driving: driving, overall: overall) do { let jsonData = try JSONEncoder().encode(matchData) var request = URLRequest(url: url) @@ -149,7 +149,6 @@ class ScoutingController: ObservableObject { func resetControllerData() { self.switches = (false, false, false, 0, 0, 0, 0) - self.matchNumber = "--" self.teamNumber = "--" self.times = [0, 0, 0] self.matchTimes = [] @@ -175,11 +174,16 @@ struct ScoutingDataExport: Codable { let overall: String } -struct MatchTime: Codable { - let score_type: Int +struct MatchTime: Codable, Identifiable { + var id = UUID() + var score_type: Int let intake: Double let travel: Double let outtake: Double + + private enum CodingKeys: String, CodingKey { + case score_type, intake, travel, outtake + } } struct MatchData: Codable { diff --git a/ios/beartracks/beartracks-scout/SettingsView.swift b/ios/beartracks/beartracks-scout/SettingsView.swift index 6ec51fa2..4da24281 100644 --- a/ios/beartracks/beartracks-scout/SettingsView.swift +++ b/ios/beartracks/beartracks-scout/SettingsView.swift @@ -36,6 +36,8 @@ struct SettingsView: View { .pickerStyle(.menu) .onChange(of: eventCodeInput) { _ in UserDefaults.standard.set(eventCodeInput, forKey: "eventCode") + controller.matchNumber = 0 + controller.teamNumber = "--" controller.getMatches { result in controller.matchList = result } @@ -54,6 +56,8 @@ struct SettingsView: View { .pickerStyle(.menu) .onChange(of: seasonInput) { _ in saveSeason() + controller.matchNumber = 0 + controller.teamNumber = "--" controller.getMatches { result in controller.matchList = result } diff --git a/ios/beartracks/beartracks-scout/StartView.swift b/ios/beartracks/beartracks-scout/StartView.swift index 83af2cda..c2956390 100644 --- a/ios/beartracks/beartracks-scout/StartView.swift +++ b/ios/beartracks/beartracks-scout/StartView.swift @@ -20,28 +20,32 @@ struct StartView: View { Text("\(UserDefaults.standard.string(forKey: "eventCode") ?? "CAFR") (\(UserDefaults.standard.string(forKey: "season") ?? "2024"))") } Section { - Picker("Match Number", selection: $controller.matchNumber) { - Text("SELECT") - .tag("--") - .disabled(true) - if !controller.matchList.isEmpty && !controller.matchList[0].Schedule.isEmpty { - ForEach(0...controller.matchList[0].Schedule.count, id: \.self) { id in - Text(String(id + 1)) - .tag(String(id + 1)) - } + Stepper { + HStack { + Text("Match Number: ") + Spacer() + Text("\(controller.matchNumber)") + } + } onIncrement: { + if controller.matchNumber < controller.matchList[0].Schedule.count { + controller.teamNumber = "--" + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + controller.matchNumber += 1; + } + } onDecrement: { + if controller.matchNumber > 1 { + controller.teamNumber = "--" + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + controller.matchNumber -= 1; } } - .pickerStyle(.menu) - .onChange(of: controller.matchNumber) { _ in - controller.teamNumber = "--" - } - if controller.matchNumber != "--" { + if controller.matchNumber != 0 { Picker("Team Number", selection: $controller.teamNumber) { Text("SELECT") .tag("--") .disabled(true) if !controller.matchList.isEmpty && !controller.matchList[0].Schedule.isEmpty { - ForEach(controller.matchList[0].Schedule[(Int(controller.matchNumber) ?? 1) - 1].teams, id: \.teamNumber) { team_entry in + ForEach(controller.matchList[0].Schedule[controller.matchNumber - 1].teams, id: \.teamNumber) { team_entry in Text(String(team_entry.teamNumber)) .tag(String(team_entry.teamNumber)) } @@ -50,60 +54,65 @@ struct StartView: View { .pickerStyle(.menu) } } - if controller.matchNumber != "--" && controller.teamNumber != "--" { + if controller.matchNumber != 0 && controller.teamNumber != "--" { Section { Text("Autonomous Period") - Stepper { - Text("Scores (\(controller.switches.6))") - } onIncrement: { - UIImpactFeedbackGenerator(style: .medium).impactOccurred() - controller.switches.6 += 1; - } onDecrement: { - if controller.switches.6 > 0 { - UIImpactFeedbackGenerator(style: .medium).impactOccurred() - controller.switches.6 -= 1; - } - } Stepper { Text("Preloaded notes handled (\(controller.switches.5))") } onIncrement: { UIImpactFeedbackGenerator(style: .medium).impactOccurred() + controller.switches.6 += 1; controller.switches.5 += 1; } onDecrement: { if controller.switches.5 > 0 { UIImpactFeedbackGenerator(style: .medium).impactOccurred() controller.switches.5 -= 1; + scoresDecrement() } } Stepper { Text("Alliance wing notes handled (\(controller.switches.4))") } onIncrement: { UIImpactFeedbackGenerator(style: .medium).impactOccurred() + controller.switches.6 += 1; controller.switches.4 += 1; } onDecrement: { if controller.switches.4 > 0 { UIImpactFeedbackGenerator(style: .medium).impactOccurred() controller.switches.4 -= 1; + scoresDecrement() } } Stepper { Text("Neutral zone notes handled (\(controller.switches.3))") } onIncrement: { UIImpactFeedbackGenerator(style: .medium).impactOccurred() + controller.switches.6 += 1; controller.switches.3 += 1; } onDecrement: { if controller.switches.3 > 0 { UIImpactFeedbackGenerator(style: .medium).impactOccurred() controller.switches.3 -= 1; + scoresDecrement() } } } + Section { + Stepper { + Text("Scores (\(controller.switches.6))") + } onIncrement: { + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + controller.switches.6 += 1; + } onDecrement: { + scoresDecrement() + } + } } Section { Button("start teleop") { controller.advanceToTab(tab: .game) } - .disabled(controller.matchNumber == "--" || controller.teamNumber == "--") + .disabled(controller.matchNumber == 0 || controller.teamNumber == "--") } } } @@ -111,6 +120,13 @@ struct StartView: View { } } } + + private func scoresDecrement() { + if controller.switches.6 > 0 { + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + controller.switches.6 -= 1; + } + } } #Preview { diff --git a/ios/beartracks/beartracks-watch Watch App/Assets.xcassets/AccentColor.colorset/Contents.json b/ios/beartracks/beartracks-watch Watch App/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..04829b2f --- /dev/null +++ b/ios/beartracks/beartracks-watch Watch App/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x96", + "green" : "0x93", + "red" : "0x0A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/beartracks/beartracks-watch Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/beartracks/beartracks-watch Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..5f660dee --- /dev/null +++ b/ios/beartracks/beartracks-watch Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "l0_sprite_4.png", + "idiom" : "universal", + "platform" : "watchos", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/beartracks/beartracks-watch Watch App/Assets.xcassets/AppIcon.appiconset/l0_sprite_4.png b/ios/beartracks/beartracks-watch Watch App/Assets.xcassets/AppIcon.appiconset/l0_sprite_4.png new file mode 100644 index 00000000..e6107849 Binary files /dev/null and b/ios/beartracks/beartracks-watch Watch App/Assets.xcassets/AppIcon.appiconset/l0_sprite_4.png differ diff --git a/ios/beartracks/beartracks-watch Watch App/Assets.xcassets/Contents.json b/ios/beartracks/beartracks-watch Watch App/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/ios/beartracks/beartracks-watch Watch App/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/beartracks/beartracks-watch Watch App/Preview Content/Preview Assets.xcassets/Contents.json b/ios/beartracks/beartracks-watch Watch App/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/ios/beartracks/beartracks-watch Watch App/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/beartracks/beartracks-watch Watch App/beartracks-watch Watch App.entitlements b/ios/beartracks/beartracks-watch Watch App/beartracks-watch Watch App.entitlements new file mode 100644 index 00000000..f3745f0e --- /dev/null +++ b/ios/beartracks/beartracks-watch Watch App/beartracks-watch Watch App.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.developer.associated-domains + + webcredentials:beartracks.io + + com.apple.security.application-groups + + group.com.jayagra.beartracks + + + diff --git a/ios/beartracks/beartracks-watch Watch App/beartracks_watchApp.swift b/ios/beartracks/beartracks-watch Watch App/beartracks_watchApp.swift new file mode 100644 index 00000000..209c4c87 --- /dev/null +++ b/ios/beartracks/beartracks-watch Watch App/beartracks_watchApp.swift @@ -0,0 +1,32 @@ +// +// beartracks_watchApp.swift +// beartracks-watch Watch App +// +// Created by Jayen Agrawal on 3/4/24. +// + +import SwiftUI + +@main +struct beartracks_watch_Watch_AppApp: App { + let settingsManager = SettingsManager.shared + @StateObject private var appState = AppState() + + var body: some Scene { + WindowGroup { + if !appState.loginRequired { + Teams() + .onAppear() { + checkLoginState { isLoggedIn in + appState.loginRequired = !isLoggedIn + } + } + .environmentObject(appState) + } else { + LoginView() + .environmentObject(appState) + .preferredColorScheme(.dark) + } + } + } +} diff --git a/ios/beartracks/beartracks-watch-Watch-App-Info.plist b/ios/beartracks/beartracks-watch-Watch-App-Info.plist new file mode 100644 index 00000000..51273a53 --- /dev/null +++ b/ios/beartracks/beartracks-watch-Watch-App-Info.plist @@ -0,0 +1,19 @@ + + + + + NSAppTransportSecurity + + NSExceptionDomains + + beartracks.io + + NSExceptionAllowsInsecureHTTPLoads + + NSIncludesSubdomains + + + + + + diff --git a/src/analyze.rs b/src/analyze.rs index 420cda52..c7a14554 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -6,7 +6,7 @@ use super::db_main; pub(crate) enum Season { S2023, - S2024 + S2024, } fn bool_to_num(value: &str) -> f64 { @@ -21,30 +21,42 @@ fn real_bool_to_num(value: bool) -> f64 { if value { return 1.0; } else { - return 0.0; + return 0.0; } } pub(crate) struct AnalysisResults { pub weight: String, - pub analysis: String + pub analysis: String, } pub fn analyze_data(data: &web::Json, season: Season) -> AnalysisResults { match season { Season::S2023 => season_2023(data).unwrap(), - Season::S2024 => season_2024(data).unwrap() + Season::S2024 => season_2024(data).unwrap(), } } fn season_2023(data: &web::Json) -> Result { let analyzer = vader_sentiment::SentimentIntensityAnalyzer::new(); let game_data = data.game.split(",").collect::>(); - let analysis_results: Vec = vec!( - analyzer.polarity_scores(data.defend.as_str()).get("compound").unwrap().clone(), - analyzer.polarity_scores(data.driving.as_str()).get("compound").unwrap().clone(), - analyzer.polarity_scores(data.overall.as_str()).get("compound").unwrap().clone() - ); + let analysis_results: Vec = vec![ + analyzer + .polarity_scores(data.defend.as_str()) + .get("compound") + .unwrap() + .clone(), + analyzer + .polarity_scores(data.driving.as_str()) + .get("compound") + .unwrap() + .clone(), + analyzer + .polarity_scores(data.overall.as_str()) + .get("compound") + .unwrap() + .clone(), + ]; let mut score: f64 = 0.0; @@ -57,16 +69,16 @@ fn season_2023(data: &web::Json) -> Result().unwrap() as f64 / 2.0; // auto points - score += bool_to_num(game_data[0]) * 6.0; // taxi - score += bool_to_num(game_data[1]) * 6.0; // score bottom - score += bool_to_num(game_data[2]) * 8.0; // score middle + score += bool_to_num(game_data[0]) * 6.0; // taxi + score += bool_to_num(game_data[1]) * 6.0; // score bottom + score += bool_to_num(game_data[2]) * 8.0; // score middle score += bool_to_num(game_data[3]) * 12.0; // score top // teleop points - score += bool_to_num(game_data[5]) * 2.0; // score bottom - score += bool_to_num(game_data[6]) * 3.0; // score mid - score += bool_to_num(game_data[7]) * 3.0; // score top - score += bool_to_num(game_data[8]) * 2.0; // coop bonus + score += bool_to_num(game_data[5]) * 2.0; // score bottom + score += bool_to_num(game_data[6]) * 3.0; // score mid + score += bool_to_num(game_data[7]) * 3.0; // score top + score += bool_to_num(game_data[8]) * 2.0; // coop bonus // grid items let mut cubes: f64 = 0.0; @@ -113,8 +125,7 @@ fn season_2023(data: &web::Json) -> Result { - } + _other => {} } } else if score_index <= 17 && item != "0" { mid += 1.0; @@ -143,8 +154,7 @@ fn season_2023(data: &web::Json) -> Result { - } + _other => {} } } else if score_index <= 26 && item != "0" { low += 1.0; @@ -173,15 +183,14 @@ fn season_2023(data: &web::Json) -> Result { - } + _other => {} } } } score += grid_wt / 1.6875; - let mps_scores: Vec = vec!( + let mps_scores: Vec = vec![ score, score + (2.0 * grid_wt), score * (cubes / 15.0), @@ -195,17 +204,14 @@ fn season_2023(data: &web::Json) -> Result = mps_scores - .iter() - .map(|float| float.to_string()) - .collect(); + let string_mps_scores: Vec = mps_scores.iter().map(|float| float.to_string()).collect(); let string_analysis_results: Vec = analysis_results - .iter() - .map(|float| float.to_string()) - .collect(); + .iter() + .map(|float| float.to_string()) + .collect(); Ok(AnalysisResults { weight: string_mps_scores.join(","), @@ -223,13 +229,26 @@ pub struct MatchTime2024 { fn season_2024(data: &web::Json) -> Result { let analyzer = vader_sentiment::SentimentIntensityAnalyzer::new(); - let game_data: Vec = serde_json::from_str(data.game.as_str()).expect("failed to convert"); - - let analysis_results: Vec = vec!( - analyzer.polarity_scores(data.defend.as_str()).get("compound").unwrap().clone(), - analyzer.polarity_scores(data.driving.as_str()).get("compound").unwrap().clone(), - analyzer.polarity_scores(data.overall.as_str()).get("compound").unwrap().clone() - ); + let game_data: Vec = + serde_json::from_str(data.game.as_str()).expect("failed to convert"); + + let analysis_results: Vec = vec![ + analyzer + .polarity_scores(data.defend.as_str()) + .get("compound") + .unwrap() + .clone(), + analyzer + .polarity_scores(data.driving.as_str()) + .get("compound") + .unwrap() + .clone(), + analyzer + .polarity_scores(data.overall.as_str()) + .get("compound") + .unwrap() + .clone(), + ]; let mut score: f64 = 0.0; @@ -258,29 +277,21 @@ fn season_2024(data: &web::Json) -> Result { if time.intake == 1.0 { climb = true; } - }, + } 4 => { if time.intake == 1.0 { buddy = true; } - }, - 5 => { - auto_center = time.intake as i64 - }, - 6 => { - auto_wing = time.intake as i64 - }, - 7 => { - auto_preload = time.intake as i64 - }, - 8 => { - auto_scores = time.intake as i64 } + 5 => auto_center = time.intake as i64, + 6 => auto_wing = time.intake as i64, + 7 => auto_preload = time.intake as i64, + 8 => auto_scores = time.intake as i64, _ => {} } intake_time += time.intake; @@ -305,23 +316,18 @@ fn season_2024(data: &web::Json) -> Result = vec!( - score, - fast_intake, - fast_travel, - fast_shoot, - fast_cycle - ); - let analysis: Vec = vec!( + let mps_scores: Vec = vec![score, fast_intake, fast_travel, fast_shoot, fast_cycle]; + + let analysis: Vec = vec![ real_bool_to_num(trap_note) as i64, real_bool_to_num(climb) as i64, real_bool_to_num(buddy) as i64, @@ -334,18 +340,13 @@ fn season_2024(data: &web::Json) -> Result = mps_scores - .iter() - .map(|float| float.to_string()) - .collect(); - - let string_analysis_results: Vec = analysis - .iter() - .map(|float| float.to_string()) - .collect(); + auto_scores, + ]; + + let string_mps_scores: Vec = mps_scores.iter().map(|float| float.to_string()).collect(); + + let string_analysis_results: Vec = + analysis.iter().map(|float| float.to_string()).collect(); Ok(AnalysisResults { weight: string_mps_scores.join(","), diff --git a/src/auth.rs b/src/auth.rs index 40fad44e..ab50d2de 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,17 +1,14 @@ -use std::sync::RwLock; use actix_http::StatusCode; -use actix_web::{web, Responder, HttpResponse}; use actix_identity::Identity; -use serde::{Serialize, Deserialize}; +use actix_web::{web, HttpResponse, Responder}; use argon2::{ - password_hash::{ - PasswordHash, - PasswordVerifier, - }, - Argon2 + password_hash::{PasswordHash, PasswordVerifier}, + Argon2, }; -use regex::Regex; use html_escape; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::sync::RwLock; use crate::db_auth; use crate::static_files; @@ -19,7 +16,7 @@ use crate::static_files; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct LoginForm { username: String, - password: String + password: String, } #[derive(Serialize, Deserialize, Clone)] @@ -27,72 +24,133 @@ pub struct CreateForm { access: String, full_name: String, username: String, - password: String + password: String, } -pub async fn create_account(pool: &db_auth::Pool, create_form: web::Json) -> impl Responder { +pub async fn create_account( + pool: &db_auth::Pool, + create_form: web::Json, +) -> impl Responder { // check password length is between 8 and 32, inclusive if create_form.password.len() >= 8 && create_form.password.len() <= 64 { // check if user is a sketchy motherfucker let regex = Regex::new(r"^[a-z0-9A-Z- ~!@#$%^&*()=+/\_[_]{}|?.,]{3,64}$").unwrap(); - if !regex.is_match(&create_form.username) || - !regex.is_match(&create_form.password) || - !regex.is_match(&create_form.full_name) - { - return HttpResponse::BadRequest().status(StatusCode::from_u16(400).unwrap()).insert_header(("Cache-Control", "no-cache")).body("{\"status\": \"you_sketchy_motherfucker\"}"); + if !regex.is_match(&create_form.username) + || !regex.is_match(&create_form.password) + || !regex.is_match(&create_form.full_name) + { + return HttpResponse::BadRequest() + .status(StatusCode::from_u16(400).unwrap()) + .insert_header(("Cache-Control", "no-cache")) + .body("{\"status\": \"you_sketchy_motherfucker\"}"); } // check if username is taken - let target_user_temp: Result = db_auth::get_user_username(pool, create_form.username.clone()).await; + let target_user_temp: Result = + db_auth::get_user_username(pool, create_form.username.clone()).await; if target_user_temp.is_ok() { - return HttpResponse::BadRequest().status(StatusCode::from_u16(409).unwrap()).insert_header(("Cache-Control", "no-cache")).body("{\"status\": \"username_taken\"}"); + return HttpResponse::BadRequest() + .status(StatusCode::from_u16(409).unwrap()) + .insert_header(("Cache-Control", "no-cache")) + .body("{\"status\": \"username_taken\"}"); } else { drop(target_user_temp); // check access key validity if create_form.access != "00000" { - let access_key_temp: Result, actix_web::Error> = db_auth::get_access_key(pool, create_form.access.clone(), db_auth::AccessKeyQuery::ById).await; + let access_key_temp: Result, actix_web::Error> = + db_auth::get_access_key( + pool, + create_form.access.clone(), + db_auth::AccessKeyQuery::ById, + ) + .await; if access_key_temp.is_err() { - return HttpResponse::BadRequest().status(StatusCode::from_u16(403).unwrap()).insert_header(("Cache-Control", "no-cache")).body("{\"status\": \"bad_access_key\"}"); + return HttpResponse::BadRequest() + .status(StatusCode::from_u16(403).unwrap()) + .insert_header(("Cache-Control", "no-cache")) + .body("{\"status\": \"bad_access_key\"}"); } else { // insert into database let access_key = access_key_temp.unwrap().first().cloned(); if let Some(valid_key) = access_key { - let user_temp: Result = db_auth::create_user(pool, valid_key.team, html_escape::encode_text(&create_form.full_name).to_string(), html_escape::encode_text(&create_form.username).to_string(), html_escape::encode_text(&create_form.password).to_string()).await; + let user_temp: Result = + db_auth::create_user( + pool, + valid_key.team, + html_escape::encode_text(&create_form.full_name).to_string(), + html_escape::encode_text(&create_form.username).to_string(), + html_escape::encode_text(&create_form.password).to_string(), + ) + .await; // send final success/failure for creation if user_temp.is_err() { - return HttpResponse::BadRequest().status(StatusCode::from_u16(500).unwrap()).insert_header(("Cache-Control", "no-cache")).body("{\"status\": \"creation_error\"}"); + return HttpResponse::BadRequest() + .status(StatusCode::from_u16(500).unwrap()) + .insert_header(("Cache-Control", "no-cache")) + .body("{\"status\": \"creation_error\"}"); } else { drop(user_temp); - return HttpResponse::Ok().status(StatusCode::from_u16(200).unwrap()).insert_header(("Cache-Control", "no-cache")).body("{\"status\": \"success\"}"); + return HttpResponse::Ok() + .status(StatusCode::from_u16(200).unwrap()) + .insert_header(("Cache-Control", "no-cache")) + .body("{\"status\": \"success\"}"); } } else { - return HttpResponse::BadRequest().status(StatusCode::from_u16(403).unwrap()).insert_header(("Cache-Control", "no-cache")).body("{\"status\": \"bad_access_key\"}"); + return HttpResponse::BadRequest() + .status(StatusCode::from_u16(403).unwrap()) + .insert_header(("Cache-Control", "no-cache")) + .body("{\"status\": \"bad_access_key\"}"); } } } else { - let user_temp: Result = db_auth::create_user(pool, 0, html_escape::encode_text(&create_form.full_name).to_string(), html_escape::encode_text(&create_form.username).to_string(), html_escape::encode_text(&create_form.password).to_string()).await; + let user_temp: Result = db_auth::create_user( + pool, + 0, + html_escape::encode_text(&create_form.full_name).to_string(), + html_escape::encode_text(&create_form.username).to_string(), + html_escape::encode_text(&create_form.password).to_string(), + ) + .await; if user_temp.is_err() { - return HttpResponse::BadRequest().status(StatusCode::from_u16(500).unwrap()).insert_header(("Cache-Control", "no-cache")).body("{\"status\": \"creation_error\"}"); + return HttpResponse::BadRequest() + .status(StatusCode::from_u16(500).unwrap()) + .insert_header(("Cache-Control", "no-cache")) + .body("{\"status\": \"creation_error\"}"); } else { drop(user_temp); - return HttpResponse::Ok().status(StatusCode::from_u16(200).unwrap()).insert_header(("Cache-Control", "no-cache")).body("{\"status\": \"success\"}"); + return HttpResponse::Ok() + .status(StatusCode::from_u16(200).unwrap()) + .insert_header(("Cache-Control", "no-cache")) + .body("{\"status\": \"success\"}"); } } } } else { - return HttpResponse::BadRequest().status(StatusCode::from_u16(413).unwrap()).insert_header(("Cache-Control", "no-cache")).body("{\"status\": \"password_length\"}"); + return HttpResponse::BadRequest() + .status(StatusCode::from_u16(413).unwrap()) + .insert_header(("Cache-Control", "no-cache")) + .body("{\"status\": \"password_length\"}"); } } -pub async fn login(pool: &db_auth::Pool, session: web::Data>, identity: Identity, login_form: web::Json) -> impl Responder { +pub async fn login( + pool: &db_auth::Pool, + session: web::Data>, + identity: Identity, + login_form: web::Json, +) -> impl Responder { // try to get target user from database - let target_user_temp: Result = db_auth::get_user_username(pool, login_form.username.clone()).await; + let target_user_temp: Result = + db_auth::get_user_username(pool, login_form.username.clone()).await; if target_user_temp.is_err() { // query error, send failure response - return HttpResponse::BadRequest().status(StatusCode::from_u16(400).unwrap()).insert_header(("Cache-Control", "no-cache")).body("{\"status\": \"bad\"}"); + return HttpResponse::BadRequest() + .status(StatusCode::from_u16(400).unwrap()) + .insert_header(("Cache-Control", "no-cache")) + .body("{\"status\": \"bad\"}"); } // query was OK, unwrap and set to target_user let target_user = target_user_temp.unwrap(); - + // ensure the target user id exists if target_user.id != 0 { // parse the hash of the user from the database @@ -100,63 +158,113 @@ pub async fn login(pool: &db_auth::Pool, session: web::Data) -> Result { - let target_user_temp: Result = db_auth::get_user_username(pool, login_form.username.clone()).await; +pub async fn delete_account( + pool: &db_auth::Pool, + login_form: web::Json, +) -> Result { + let target_user_temp: Result = + db_auth::get_user_username(pool, login_form.username.clone()).await; if target_user_temp.is_err() { - return Ok(HttpResponse::BadRequest().status(StatusCode::from_u16(400).unwrap()).insert_header(("Cache-Control", "no-cache")).body("{\"status\": \"bad\"}")) + return Ok(HttpResponse::BadRequest() + .status(StatusCode::from_u16(400).unwrap()) + .insert_header(("Cache-Control", "no-cache")) + .body("{\"status\": \"bad\"}")); } let target_user = target_user_temp.unwrap(); - if target_user.id != 0 { + if target_user.id != 0 { let parsed_hash = PasswordHash::new(&target_user.pass_hash); if parsed_hash.is_err() { - return Ok(HttpResponse::BadRequest().status(StatusCode::from_u16(400).unwrap()).insert_header(("Cache-Control", "no-cache")).body("{\"status\": \"bad\"}")) + return Ok(HttpResponse::BadRequest() + .status(StatusCode::from_u16(400).unwrap()) + .insert_header(("Cache-Control", "no-cache")) + .body("{\"status\": \"bad\"}")); } - if Argon2::default().verify_password(login_form.password.as_bytes(), &parsed_hash.unwrap()).is_ok() { - Ok(HttpResponse::Ok() - .json( - db_auth::execute_manage_user(&pool, db_auth::UserManageAction::DeleteUser, [target_user.id.to_string(), "".to_string()]).await? - )) + if Argon2::default() + .verify_password(login_form.password.as_bytes(), &parsed_hash.unwrap()) + .is_ok() + { + Ok(HttpResponse::Ok().json( + db_auth::execute_manage_user( + &pool, + db_auth::UserManageAction::DeleteUser, + [target_user.id.to_string(), "".to_string()], + ) + .await?, + )) } else { - Ok(HttpResponse::BadRequest().status(StatusCode::from_u16(400).unwrap()).insert_header(("Cache-Control", "no-cache")).body("{\"status\": \"bad\"}")) + Ok(HttpResponse::BadRequest() + .status(StatusCode::from_u16(400).unwrap()) + .insert_header(("Cache-Control", "no-cache")) + .body("{\"status\": \"bad\"}")) } } else { - Ok(HttpResponse::BadRequest().status(StatusCode::from_u16(400).unwrap()).insert_header(("Cache-Control", "no-cache")).body("{\"status\": \"bad\"}")) + Ok(HttpResponse::BadRequest() + .status(StatusCode::from_u16(400).unwrap()) + .insert_header(("Cache-Control", "no-cache")) + .body("{\"status\": \"bad\"}")) } } -pub async fn logout(session: web::Data>, identity: Identity) -> HttpResponse { +pub async fn logout( + session: web::Data>, + identity: Identity, +) -> HttpResponse { // if session exists, proceed if let Some(id) = identity.identity() { // forget identity @@ -166,7 +274,7 @@ pub async fn logout(session: web::Data>, identity: Ident // log::info!("user {} logged out", user.username); } } - + // send user to login page static_files::static_login().await -} \ No newline at end of file +} diff --git a/src/casino.rs b/src/casino.rs index d2a26ab6..17efbf1b 100644 --- a/src/casino.rs +++ b/src/casino.rs @@ -1,17 +1,21 @@ use actix::Message; use actix::{Actor, StreamHandler}; -use actix_web::{web, error, HttpRequest, HttpResponse, Error}; +use actix_web::{error, web, Error, HttpRequest, HttpResponse}; use actix_web_actors::ws; +use rand::{prelude::SliceRandom, Rng}; use rusqlite; -use rand::{Rng, prelude::SliceRandom}; use tokio; -use crate::{db_auth, Databases}; use crate::db_transact; +use crate::{db_auth, Databases}; const SPIN_THING_SPINS: [i64; 12] = [10, 20, 50, -15, -25, -35, -100, -50, 100, 250, -1000, 1250]; -pub async fn spin_thing(auth_pool: &db_auth::Pool, transact_pool: &db_transact::Pool, user: db_auth::User) -> Result { +pub async fn spin_thing( + auth_pool: &db_auth::Pool, + transact_pool: &db_transact::Pool, + user: db_auth::User, +) -> Result { // we need access to auth and transact because we're inserting a transaction let auth_pool = auth_pool.clone(); let transact_pool = transact_pool.clone(); @@ -24,14 +28,16 @@ pub async fn spin_thing(auth_pool: &db_auth::Pool, transact_pool: &db_transact:: .await? .map_err(error::ErrorInternalServerError)?; - web::block(move || { - spin_thing_process(auth_conn, transact_conn, user) - }) - .await? - .map_err(error::ErrorInternalServerError) + web::block(move || spin_thing_process(auth_conn, transact_conn, user)) + .await? + .map_err(error::ErrorInternalServerError) } -fn spin_thing_process(auth_conn: db_auth::Connection, transact_conn: db_transact::Connection, user: db_auth::User) -> Result { +fn spin_thing_process( + auth_conn: db_auth::Connection, + transact_conn: db_transact::Connection, + user: db_auth::User, +) -> Result { // rig the spin let mut spin: i64 = rand::thread_rng().gen_range(0..11); for _i in 0..3 { @@ -48,7 +54,16 @@ fn spin_thing_process(auth_conn: db_auth::Connection, transact_conn: db_transact // insert transaction & update points db_auth::update_points(auth_conn, user.id, SPIN_THING_SPINS[spin as usize])?; - db_transact::insert_transaction(transact_conn, db_transact::Transact { id: 0, user_id: user.id, trans_type: 0x1500, amount: SPIN_THING_SPINS[spin as usize], time: "".to_string() })?; + db_transact::insert_transaction( + transact_conn, + db_transact::Transact { + id: 0, + user_id: user.id, + trans_type: 0x1500, + amount: SPIN_THING_SPINS[spin as usize], + time: "".to_string(), + }, + )?; // send spin to client Ok(format!("{{\"spin\": {}}}", spin)) @@ -60,14 +75,16 @@ fn spin_thing_process(auth_conn: db_auth::Connection, transact_conn: db_transact // suit and value array constants const SUITS: [&str; 4] = ["h", "d", "c", "s"]; -const VALUES: [&str; 13] = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]; +const VALUES: [&str; 13] = [ + "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", +]; #[derive(Clone, Debug)] struct BlackjackSession { game: BlackjackGame, user_id: i64, auth_db: db_auth::Pool, - transact_db: db_transact::Pool + transact_db: db_transact::Pool, } #[derive(Clone, Debug)] @@ -135,8 +152,14 @@ impl Actor for BlackjackSession { } } -impl StreamHandler> for BlackjackSession { - fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { +impl StreamHandler> + for BlackjackSession +{ + fn handle( + &mut self, + msg: Result, + ctx: &mut Self::Context, + ) { match msg { // text messages Ok(actix_http::ws::Message::Text(text)) => { @@ -148,7 +171,8 @@ impl StreamHandler, target: &str, ctx: &mut ws::WebsocketContext) -> i64 { + fn get_card( + &mut self, + hand: &mut Vec, + target: &str, + ctx: &mut ws::WebsocketContext, + ) -> i64 { // draw card let new_card = self.new_card(); // insert into hand @@ -245,14 +284,26 @@ impl BlackjackSession { self.game.player.hand = hand.clone(); self.game.player.score = new_score; // send card to client - ctx.text(format!(r#"{{"card": {{"suit": "{}", "value": "{}"}}, "target": "{}{}"}}"#, new_card.suit, new_card.value, target, self.game.player.hand.len())); + ctx.text(format!( + r#"{{"card": {{"suit": "{}", "value": "{}"}}, "target": "{}{}"}}"#, + new_card.suit, + new_card.value, + target, + self.game.player.hand.len() + )); } else { // update game instance self.game.dealer.hand = hand.clone(); self.game.dealer.score = new_score; // send dealer card to client // this is safe because the dealer only draws once game is over - ctx.text(format!(r#"{{"card": {{"suit": "{}", "value": "{}"}}, "target": "{}{}"}}"#, new_card.suit, new_card.value, target, self.game.dealer.hand.len())); + ctx.text(format!( + r#"{{"card": {{"suit": "{}", "value": "{}"}}, "target": "{}{}"}}"#, + new_card.suit, + new_card.value, + target, + self.game.dealer.hand.len() + )); } new_score @@ -318,8 +369,7 @@ impl BlackjackSession { credit_points(auth_pool_clone, transact_pool_clone, user_id_clone, -10) .await .unwrap_or("bad".to_string()); - } - ); + }); // if dealer busts } else if self.game.dealer.score > 21 { result = "WD".to_string(); @@ -327,8 +377,7 @@ impl BlackjackSession { credit_points(auth_pool_clone, transact_pool_clone, user_id_clone, 10) .await .unwrap_or("bad".to_string()); - } - ); + }); // if player score is more than dealer score } else if self.game.player.score > self.game.dealer.score { result = "WN".to_string(); @@ -336,8 +385,7 @@ impl BlackjackSession { credit_points(auth_pool_clone, transact_pool_clone, user_id_clone, 10) .await .unwrap_or("bad".to_string()); - } - ); + }); // if dealer score is more than player score } else if self.game.player.score < self.game.dealer.score { result = "LS".to_string(); @@ -345,8 +393,7 @@ impl BlackjackSession { credit_points(auth_pool_clone, transact_pool_clone, user_id_clone, -10) .await .unwrap_or("bad".to_string()); - } - ); + }); // draw } else { result = "DR".to_string(); @@ -354,18 +401,25 @@ impl BlackjackSession { credit_points(auth_pool_clone, transact_pool_clone, user_id_clone, 0) .await .unwrap_or("bad".to_string()); - } - ); + }); } // send result ctx.text(format!(r#"{{"result": "{}"}}"#, result)); // close socket - ctx.close(Some(ws::CloseReason { code: ws::CloseCode::Normal, description: Some("".to_string()) })); + ctx.close(Some(ws::CloseReason { + code: ws::CloseCode::Normal, + description: Some("".to_string()), + })); } } -async fn credit_points(auth_pool: db_auth::Pool, transact_pool: db_transact::Pool, user_id: i64, amount: i64) -> Result { +async fn credit_points( + auth_pool: db_auth::Pool, + transact_pool: db_transact::Pool, + user_id: i64, + amount: i64, +) -> Result { let auth_conn = web::block(move || auth_pool.get()) .await? .map_err(error::ErrorInternalServerError)?; @@ -374,45 +428,57 @@ async fn credit_points(auth_pool: db_auth::Pool, transact_pool: db_transact::Poo .await? .map_err(error::ErrorInternalServerError)?; - web::block(move || { - credit_points_run(auth_conn, transact_conn, user_id, amount) - }) - .await? - .map_err(error::ErrorInternalServerError) + web::block(move || credit_points_run(auth_conn, transact_conn, user_id, amount)) + .await? + .map_err(error::ErrorInternalServerError) } -fn credit_points_run(auth_conn: db_auth::Connection, transact_conn: db_transact::Connection, user_id: i64, amount: i64) -> Result { +fn credit_points_run( + auth_conn: db_auth::Connection, + transact_conn: db_transact::Connection, + user_id: i64, + amount: i64, +) -> Result { db_auth::update_points(auth_conn, user_id, amount)?; db_transact::insert_transaction( - transact_conn, + transact_conn, db_transact::Transact { id: 0, user_id, trans_type: 0x1502, amount, - time: "".to_string() - } + time: "".to_string(), + }, )?; Ok("ok".to_string()) } // websocket route -pub async fn websocket_route(req: HttpRequest, stream: web::Payload, db: web::Data, user: db_auth::User) -> Result { +pub async fn websocket_route( + req: HttpRequest, + stream: web::Payload, + db: web::Data, + user: db_auth::User, +) -> Result { // start websocket connection with clean game state - ws::start(BlackjackSession { - game: BlackjackGame { - player: Player { - hand: Vec::new(), - score: 0 - }, - dealer: Player { - hand: Vec::new(), - score: 0 + ws::start( + BlackjackSession { + game: BlackjackGame { + player: Player { + hand: Vec::new(), + score: 0, + }, + dealer: Player { + hand: Vec::new(), + score: 0, + }, }, + user_id: user.id, + auth_db: db.auth.clone(), + transact_db: db.transact.clone(), }, - user_id: user.id, - auth_db: db.auth.clone(), - transact_db: db.transact.clone() - }, &req, stream) -} \ No newline at end of file + &req, + stream, + ) +} diff --git a/src/db_auth.rs b/src/db_auth.rs index 0a8708df..7e8de8ae 100644 --- a/src/db_auth.rs +++ b/src/db_auth.rs @@ -1,16 +1,12 @@ -use std::str; use actix_web::{error, web, Error}; -use rusqlite::{Statement, params}; -use serde::{Serialize, Deserialize}; -use serde_json; use argon2::{ - password_hash::{ - rand_core::OsRng, - PasswordHasher, - SaltString - }, - Argon2 + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Argon2, }; +use rusqlite::{params, Statement}; +use serde::{Deserialize, Serialize}; +use serde_json; +use std::str; use webauthn_rs::prelude::*; use crate::db_main; @@ -28,7 +24,7 @@ pub type Connection = r2d2::PooledConnection, rusqlite::Error>; pub enum AuthData { - GetUserScores + GetUserScores, } pub async fn execute_scores(pool: &Pool, query: AuthData) -> Result, Error> { @@ -38,10 +34,8 @@ pub async fn execute_scores(pool: &Pool, query: AuthData) -> Result get_user_scores(conn) - } + web::block(move || match query { + AuthData::GetUserScores => get_user_scores(conn), }) .await? .map_err(error::ErrorInternalServerError) @@ -59,7 +53,7 @@ fn get_score_rows(mut statement: Statement) -> PointQueryResult { id: row.get(0)?, username: row.get(1)?, team: row.get(2)?, - score: row.get(3)? + score: row.get(3)?, }) }) .and_then(Iterator::collect) @@ -77,14 +71,14 @@ pub struct User { pub admin: String, pub team_admin: i64, pub access_ok: String, - pub score: i64 + pub score: i64, } #[derive(Serialize, Deserialize, Clone)] pub struct AccessKey { pub id: i64, pub key: i64, - pub team: i64 + pub team: i64, } pub async fn get_user_username(pool: &Pool, username: String) -> Result { @@ -94,11 +88,9 @@ pub async fn get_user_username(pool: &Pool, username: String) -> Result Result { @@ -108,11 +100,9 @@ pub async fn get_user_id(pool: &Pool, id: String) -> Result { .await? .map_err(error::ErrorInternalServerError)?; - web::block(move || { - get_user_id_entry(conn, id) - }) - .await? - .map_err(error::ErrorInternalServerError) + web::block(move || get_user_id_entry(conn, id)) + .await? + .map_err(error::ErrorInternalServerError) } fn get_user_id_entry(conn: Connection, id: String) -> Result { @@ -155,21 +145,23 @@ fn get_user_username_entry(conn: Connection, username: String) -> Result Result, Error> { +pub async fn get_access_key( + pool: &Pool, + key: String, + query: AccessKeyQuery, +) -> Result, Error> { let pool = pool.clone(); let conn = web::block(move || pool.get()) .await? .map_err(error::ErrorInternalServerError)?; - web::block(move || { - match query { - AccessKeyQuery::ById => get_access_key_entry(conn, key), - AccessKeyQuery::AllKeys => get_access_key_all(conn), - } + web::block(move || match query { + AccessKeyQuery::ById => get_access_key_entry(conn, key), + AccessKeyQuery::AllKeys => get_access_key_all(conn), }) .await? .map_err(error::ErrorInternalServerError) @@ -199,7 +191,13 @@ fn get_access_key_all(conn: Connection) -> Result, rusqlite::Erro .and_then(Iterator::collect) } -pub async fn create_user(pool: &Pool, team: i64, full_name: String, username: String, password: String) -> Result { +pub async fn create_user( + pool: &Pool, + team: i64, + full_name: String, + username: String, + password: String, +) -> Result { let pool = pool.clone(); let conn = web::block(move || pool.get()) .await? @@ -222,16 +220,29 @@ pub async fn create_user(pool: &Pool, team: i64, full_name: String, username: St admin: "false".to_string(), team_admin: 0, access_ok: "true".to_string(), - score: 0 - }).map_err(rusqlite::Error::NulError) + score: 0, + }) + .map_err(rusqlite::Error::NulError); } - create_user_entry(conn, team, full_name, username, hashed_password.unwrap().to_string()) + create_user_entry( + conn, + team, + full_name, + username, + hashed_password.unwrap().to_string(), + ) }) .await? .map_err(error::ErrorInternalServerError) } -fn create_user_entry(conn: Connection, team: i64, full_name: String, username: String, password_hash: String) -> Result { +fn create_user_entry( + conn: Connection, + team: i64, + full_name: String, + username: String, + password_hash: String, +) -> Result { let mut stmt = conn.prepare("INSERT INTO users (username, current_challenge, full_name, team, data, pass_hash, admin, team_admin, access_ok, score) VALUES (?, '', ?, ?, '', ?, 'false', 0, 'true', 0);")?; let mut new_user = User { id: 0, @@ -244,7 +255,7 @@ fn create_user_entry(conn: Connection, team: i64, full_name: String, username: S admin: "false".to_string(), team_admin: 0, access_ok: "true".to_string(), - score: 0 + score: 0, }; stmt.execute(params![ new_user.username, @@ -256,30 +267,44 @@ fn create_user_entry(conn: Connection, team: i64, full_name: String, username: S Ok(new_user) } -pub fn update_points(conn: Connection, user_id: i64, inc: i64) -> Result { +pub fn update_points( + conn: Connection, + user_id: i64, + inc: i64, +) -> Result { let mut stmt = conn.prepare("UPDATE users SET score = score + ?1 WHERE id = ?2;")?; stmt.execute(params![inc, user_id])?; - Ok(db_main::Id { id: conn.last_insert_rowid() }) + Ok(db_main::Id { + id: conn.last_insert_rowid(), + }) } -pub async fn update_user_data(pool: &Pool, user_id: i64, new_data: String) -> Result { +pub async fn update_user_data( + pool: &Pool, + user_id: i64, + new_data: String, +) -> Result { let pool = pool.clone(); let conn = web::block(move || pool.get()) .await? .map_err(error::ErrorInternalServerError)?; - web::block(move || { - update_user_data_transaction(conn, user_id, new_data) - }) - .await? - .map_err(error::ErrorInternalServerError) + web::block(move || update_user_data_transaction(conn, user_id, new_data)) + .await? + .map_err(error::ErrorInternalServerError) } -pub fn update_user_data_transaction(conn: Connection, user_id: i64, new_data: String) -> Result { +pub fn update_user_data_transaction( + conn: Connection, + user_id: i64, + new_data: String, +) -> Result { let mut stmt: Statement<'_> = conn.prepare("UPDATE users SET data = ?1 WHERE id = ?2;")?; stmt.execute(params![new_data, user_id])?; - Ok(db_main::Id { id: conn.last_insert_rowid() }) + Ok(db_main::Id { + id: conn.last_insert_rowid(), + }) } #[derive(Serialize, Deserialize, Clone)] @@ -289,28 +314,30 @@ pub struct UserPartial { pub team: i64, pub admin: String, pub team_admin: i64, - pub score: i64 + pub score: i64, } pub enum UserQueryType { All, - Team + Team, } type UserPartialQuery = Result, rusqlite::Error>; -pub async fn execute_get_users_mgmt(pool: &Pool, query: UserQueryType, user: User) -> Result, Error> { +pub async fn execute_get_users_mgmt( + pool: &Pool, + query: UserQueryType, + user: User, +) -> Result, Error> { let pool = pool.clone(); let conn = web::block(move || pool.get()) .await? .map_err(error::ErrorInternalServerError)?; - web::block(move || { - match query { - UserQueryType::All => get_users_mgmt(conn, user), - UserQueryType::Team => get_users_team_mgmt(conn, user), - } + web::block(move || match query { + UserQueryType::All => get_users_mgmt(conn, user), + UserQueryType::Team => get_users_team_mgmt(conn, user), }) .await? .map_err(error::ErrorInternalServerError) @@ -345,49 +372,66 @@ pub enum UserManageAction { DeleteUser, ModifyAdmin, ModifyTeamAdmin, - ModifyPoints + ModifyPoints, } -pub async fn execute_manage_user(pool: &Pool, action: UserManageAction, params: [String; 2]) -> Result { +pub async fn execute_manage_user( + pool: &Pool, + action: UserManageAction, + params: [String; 2], +) -> Result { let pool = pool.clone(); let conn = web::block(move || pool.get()) .await? .map_err(error::ErrorInternalServerError)?; - web::block(move || { - match action { - UserManageAction::DeleteUser => manage_delete_user(conn, params), - UserManageAction::ModifyAdmin => manage_modify_admin(conn, params), - UserManageAction::ModifyTeamAdmin => manage_modify_team_admin(conn, params), - UserManageAction::ModifyPoints => manage_modify_points(conn, params), - } + web::block(move || match action { + UserManageAction::DeleteUser => manage_delete_user(conn, params), + UserManageAction::ModifyAdmin => manage_modify_admin(conn, params), + UserManageAction::ModifyTeamAdmin => manage_modify_team_admin(conn, params), + UserManageAction::ModifyPoints => manage_modify_points(conn, params), }) .await? .map_err(error::ErrorInternalServerError) } -fn manage_delete_user(connection: Connection, params: [String; 2]) -> Result { +fn manage_delete_user( + connection: Connection, + params: [String; 2], +) -> Result { let stmt = connection.prepare("DELETE FROM users WHERE id=?1 AND score!=?2;")?; execute_manage_action(stmt, params) } -fn manage_modify_admin(connection: Connection, params: [String; 2]) -> Result { +fn manage_modify_admin( + connection: Connection, + params: [String; 2], +) -> Result { let stmt = connection.prepare("UPDATE users SET admin=?1 WHERE id=?2;")?; execute_manage_action(stmt, params) } -fn manage_modify_team_admin(connection: Connection, params: [String; 2]) -> Result { +fn manage_modify_team_admin( + connection: Connection, + params: [String; 2], +) -> Result { let stmt = connection.prepare("UPDATE users SET team_admin=?1 WHERE id=?2;")?; execute_manage_action(stmt, params) } -fn manage_modify_points(connection: Connection, params: [String; 2]) -> Result { +fn manage_modify_points( + connection: Connection, + params: [String; 2], +) -> Result { let stmt = connection.prepare("UPDATE users SET score = score + ?1 WHERE id=?2;")?; execute_manage_action(stmt, params) } -fn execute_manage_action(mut statement: Statement, params: [String; 2]) -> Result { +fn execute_manage_action( + mut statement: Statement, + params: [String; 2], +) -> Result { if statement.execute(params).is_ok() { Ok("{\"status\":3206}".to_string()) } else { @@ -402,11 +446,9 @@ pub async fn delete_access_key(pool: &Pool, id: String) -> Result .await? .map_err(error::ErrorInternalServerError)?; - web::block(move || { - delete_access_key_sql(conn, id) - }) - .await? - .map_err(error::ErrorInternalServerError) + web::block(move || delete_access_key_sql(conn, id)) + .await? + .map_err(error::ErrorInternalServerError) } fn delete_access_key_sql(conn: Connection, id: String) -> Result { @@ -422,14 +464,16 @@ pub async fn create_access_key(pool: &Pool, key: String, team: String) -> Result .await? .map_err(error::ErrorInternalServerError)?; - web::block(move || { - create_access_key_sql(conn, key, team) - }) - .await? - .map_err(error::ErrorInternalServerError) + web::block(move || create_access_key_sql(conn, key, team)) + .await? + .map_err(error::ErrorInternalServerError) } -fn create_access_key_sql(conn: Connection, key: String, team: String) -> Result { +fn create_access_key_sql( + conn: Connection, + key: String, + team: String, +) -> Result { let mut stmt = conn.prepare("INSERT INTO accessKeys (key, team) VALUES (?, ?);")?; stmt.execute(params![key, team])?; Ok("{\"status\": 3207}".to_string()) @@ -442,14 +486,16 @@ pub async fn update_access_key(pool: &Pool, key: String, id: String) -> Result Result { +fn update_access_key_sql( + conn: Connection, + key: String, + id: String, +) -> Result { let mut stmt = conn.prepare("UPDATE accessKeys SET key=?1 WHERE id=?2")?; stmt.execute(params![key, id])?; Ok("{\"status\": 3207}".to_string()) @@ -462,11 +508,9 @@ pub async fn get_passkeys(pool: &Pool, id: String) -> Result, Error .await? .map_err(error::ErrorInternalServerError)?; - web::block(move || { - get_passkey_data(conn, id) - }) - .await? - .map_err(error::ErrorInternalServerError) + web::block(move || get_passkey_data(conn, id)) + .await? + .map_err(error::ErrorInternalServerError) } fn get_passkey_data(conn: Connection, id: String) -> Result, rusqlite::Error> { @@ -474,9 +518,7 @@ fn get_passkey_data(conn: Connection, id: String) -> Result, rusqli statement .query_map([id], |row| { let passkey_text: String = row.get(0)?; - Ok( - serde_json::from_str(str::from_utf8(passkey_text.as_bytes()).unwrap()).unwrap() - ) + Ok(serde_json::from_str(str::from_utf8(passkey_text.as_bytes()).unwrap()).unwrap()) }) .and_then(Iterator::collect) } @@ -488,15 +530,17 @@ pub async fn set_passkey(pool: &Pool, passkey: Passkey, user_id: i64) -> Result< .await? .map_err(error::ErrorInternalServerError)?; - web::block(move || { - set_passkey_data(conn, passkey, user_id) - }) - .await? - .map_err(error::ErrorInternalServerError) + web::block(move || set_passkey_data(conn, passkey, user_id)) + .await? + .map_err(error::ErrorInternalServerError) } -fn set_passkey_data(conn: Connection, passkey: Passkey, user_id: i64) -> Result { +fn set_passkey_data( + conn: Connection, + passkey: Passkey, + user_id: i64, +) -> Result { let mut statement = conn.prepare("INSERT INTO passkeys (user_id, passkey) VALUES (?1, ?2);")?; statement.execute(params![user_id, serde_json::to_string(&passkey).unwrap()])?; Ok(conn.last_insert_rowid()) -} \ No newline at end of file +} diff --git a/src/db_main.rs b/src/db_main.rs index b51a5efd..cbb26752 100644 --- a/src/db_main.rs +++ b/src/db_main.rs @@ -1,5 +1,6 @@ use actix_web::{error, web, Error}; -use rusqlite::{Statement, params}; +use regex::Regex; +use rusqlite::{params, Statement}; use serde::{Deserialize, Serialize}; use crate::db_auth; @@ -50,7 +51,7 @@ pub enum Main { }, Id { id: i64, - } + }, } // pool and connection types @@ -70,11 +71,16 @@ pub enum MainData { BriefMatch, BriefUser, GetTeams, - Id + Id, + GetAllData, } // execute query -pub async fn execute(pool: &Pool, query: MainData, path: web::Path) -> Result, Error> { +pub async fn execute( + pool: &Pool, + query: MainData, + path: web::Path, +) -> Result, Error> { // clone pool let pool = pool.clone(); @@ -84,18 +90,17 @@ pub async fn execute(pool: &Pool, query: MainData, path: web::Path) -> R .map_err(error::ErrorInternalServerError)?; // run query function based on provided enum - web::block(move || { - match query { - MainData::GetDataDetailed => get_data_detailed(conn, path), - MainData::DataExists => get_submission_exists(conn, path), - MainData::BriefSeason => get_brief_season(conn, path), - MainData::BriefEvent => get_brief_event(conn, path), - MainData::BriefTeam => get_brief_team(conn, path), - MainData::BriefMatch => get_brief_match(conn, path), - MainData::BriefUser => get_brief_user(conn, path), - MainData::GetTeams => get_all_teams(conn, path), - MainData::Id => get_main_ids(conn, path), - } + web::block(move || match query { + MainData::GetDataDetailed => get_data_detailed(conn, path), + MainData::DataExists => get_submission_exists(conn, path), + MainData::BriefSeason => get_brief_season(conn, path), + MainData::BriefEvent => get_brief_event(conn, path), + MainData::BriefTeam => get_brief_team(conn, path), + MainData::BriefMatch => get_brief_match(conn, path), + MainData::BriefUser => get_brief_user(conn, path), + MainData::GetTeams => get_all_teams(conn, path), + MainData::Id => get_main_ids(conn, path), + MainData::GetAllData => get_all_data(conn, path), }) .await? .map_err(error::ErrorInternalServerError) @@ -108,6 +113,13 @@ fn get_data_detailed(conn: Connection, path: web::Path) -> QueryResult { get_rows(stmt, [args[0].parse::().unwrap()]) } +// get all data +fn get_all_data(conn: Connection, path: web::Path) -> QueryResult { + let args = path.split("/").collect::>(); + let stmt = conn.prepare("SELECT * FROM main WHERE season=:season;")?; + get_rows(stmt, [args[0].parse::().unwrap()]) +} + // get the id, team, and match number based on an id. used to confirm submission fn get_submission_exists(conn: Connection, path: web::Path) -> QueryResult { let args = path.split("/").collect::>(); @@ -152,7 +164,8 @@ fn get_brief_user(conn: Connection, path: web::Path) -> QueryResult { // get weight and team number for all teams at an event, for performance rankings fn get_all_teams(conn: Connection, path: web::Path) -> QueryResult { let args = path.split("/").collect::>(); - let stmt = conn.prepare("SELECT id, team, weight FROM main WHERE season=?1 AND event=?2 GROUP BY team;")?; + let stmt = conn + .prepare("SELECT id, team, weight FROM main WHERE season=?1 AND event=?2 GROUP BY team;")?; get_team_rows(stmt, [args[0], args[1]]) } @@ -166,7 +179,7 @@ fn get_main_ids(conn: Connection, _path: web::Path) -> QueryResult { fn get_exists_row(mut statement: Statement, params: [i64; 1]) -> QueryResult { statement .query_map(params, |row| { - Ok(Main::Exists { + Ok(Main::Exists { id: row.get(0)?, team: row.get(1)?, match_num: row.get(2)?, @@ -179,7 +192,7 @@ fn get_exists_row(mut statement: Statement, params: [i64; 1]) -> QueryResult { fn get_rows(mut statement: Statement, params: [i64; 1]) -> QueryResult { statement .query_map(params, |row| { - Ok(Main::FullMain { + Ok(Main::FullMain { id: row.get(0)?, event: row.get(1)?, season: row.get(2)?, @@ -194,7 +207,7 @@ fn get_rows(mut statement: Statement, params: [i64; 1]) -> QueryResult { name: row.get(11)?, from_team: row.get(12)?, weight: row.get(13)?, - analysis: row.get(14)? + analysis: row.get(14)?, }) }) .and_then(Iterator::collect) @@ -204,7 +217,7 @@ fn get_rows(mut statement: Statement, params: [i64; 1]) -> QueryResult { fn get_brief_rows(mut statement: Statement, params: [&str; 3]) -> QueryResult { statement .query_map(params, |row| { - Ok(Main::Brief { + Ok(Main::Brief { id: row.get(0)?, event: row.get(1)?, season: row.get(2)?, @@ -226,7 +239,7 @@ fn get_team_rows(mut statement: Statement, params: [&str; 2]) -> QueryResult { Ok(Main::Team { id: row.get(0)?, team: row.get(1)?, - weight: row.get(2)? + weight: row.get(2)?, }) }) .and_then(Iterator::collect) @@ -235,11 +248,7 @@ fn get_team_rows(mut statement: Statement, params: [&str; 2]) -> QueryResult { // results to enum for existing data fn get_id_rows(mut statement: Statement) -> QueryResult { statement - .query_map([], |row| { - Ok(Main::Id { - id: row.get(0)? - }) - }) + .query_map([], |row| Ok(Main::Id { id: row.get(0)? })) .and_then(Iterator::collect) } @@ -255,11 +264,8 @@ pub async fn get_team_numbers(pool: &Pool, season: String) -> Result, E // run query function based on provided enum web::block(move || { let mut stmt = conn.prepare("SELECT team FROM main WHERE season=?1 ORDER BY id DESC;")?; - stmt - .query_map([season], |row| { - Ok(row.get(0)?) - }) - .and_then(Iterator::collect) + stmt.query_map([season], |row| Ok(row.get(0)?)) + .and_then(Iterator::collect) }) .await? .map_err(error::ErrorInternalServerError) @@ -285,7 +291,13 @@ pub struct Id { pub id: i64, } -pub async fn execute_insert(pool: &Pool, transact_pool: &Pool, auth_pool: &Pool, data: web::Json, user: db_auth::User) -> Result { +pub async fn execute_insert( + pool: &Pool, + transact_pool: &Pool, + auth_pool: &Pool, + data: web::Json, + user: db_auth::User, +) -> Result { // clone pools for all databases let pool = pool.clone(); let transact_pool = transact_pool.clone(); @@ -304,14 +316,18 @@ pub async fn execute_insert(pool: &Pool, transact_pool: &Pool, auth_pool: &Pool, .await? .map_err(error::ErrorInternalServerError)?; - web::block(move || { - insert_main_data(conn, transact_conn, auth_conn, &data, user) - }) - .await? - .map_err(error::ErrorInternalServerError) + web::block(move || insert_main_data(conn, transact_conn, auth_conn, &data, user)) + .await? + .map_err(error::ErrorInternalServerError) } -fn insert_main_data(conn: Connection, transact_conn: Connection, auth_conn: Connection, data: &web::Json, user: db_auth::User) -> Result { +fn insert_main_data( + conn: Connection, + transact_conn: Connection, + auth_conn: Connection, + data: &web::Json, + user: db_auth::User, +) -> Result { // analyze the data let analysis_results: analyze::AnalysisResults; analysis_results = match data.season { @@ -321,19 +337,43 @@ fn insert_main_data(conn: Connection, transact_conn: Connection, auth_conn: Conn }; // create mutable response object - let mut inserted_row = Id { - id: 0 - }; + let mut inserted_row = Id { id: 0 }; + let regex = Regex::new(r"[;'`:/*]").unwrap(); // insert data into database let mut stmt = conn.prepare("INSERT INTO main (event, season, team, match_num, level, game, defend, driving, overall, user_id, name, from_team, weight, analysis) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);")?; - stmt.execute(params![data.event, data.season, data.team, data.match_num, data.level, data.game, data.defend, data.driving, data.overall, user.id, user.full_name, user.team, analysis_results.weight, analysis_results.analysis])?; + stmt.execute(params![ + data.event, + data.season, + data.team, + data.match_num, + data.level, + data.game, + regex.replace_all(data.defend.as_str(), "-"), + regex.replace_all(data.driving.as_str(), "-"), + regex.replace_all(data.overall.as_str(), "-"), + user.id, + user.full_name, + user.team, + analysis_results.weight, + analysis_results.analysis + ])?; // update response object with the correct ID inserted_row.id = conn.last_insert_rowid(); // insert transaction and update points. they're unused variables- i don't care if the points failed to credit - let _inserted = db_transact::insert_transaction(transact_conn, db_transact::Transact { id: 0, user_id: user.id, trans_type: 0x1000, amount: 25, time: "".to_string() }).expect("oop"); + let _inserted = db_transact::insert_transaction( + transact_conn, + db_transact::Transact { + id: 0, + user_id: user.id, + trans_type: 0x1000, + amount: 25, + time: "".to_string(), + }, + ) + .expect("oop"); let _updated = db_auth::update_points(auth_conn, user.id, 25).expect("oop"); // return response object @@ -341,7 +381,12 @@ fn insert_main_data(conn: Connection, transact_conn: Connection, auth_conn: Conn } // function to delete data for users with admin access -pub async fn delete_by_id(pool: &Pool, transact_pool: &Pool, auth_pool: &Pool, path: web::Path) -> Result { +pub async fn delete_by_id( + pool: &Pool, + transact_pool: &Pool, + auth_pool: &Pool, + path: web::Path, +) -> Result { // clone pools for all three databases let pool = pool.clone(); let transact_pool = transact_pool.clone(); @@ -366,17 +411,25 @@ pub async fn delete_by_id(pool: &Pool, transact_pool: &Pool, auth_pool: &Pool, p // prepare statement let mut stmt = conn.prepare("DELETE FROM main WHERE id=?1 RETURNING user_id;")?; // run query, mapping the result to a single Id object (containing the user id, not submission id) that will used to deduct points - let execution = stmt - .query_row(params![target_id.parse::().unwrap()], |row| { - Ok(Id { - id: row.get(0)?, - }) - }); + let execution = stmt.query_row(params![target_id.parse::().unwrap()], |row| { + Ok(Id { id: row.get(0)? }) + }); if execution.is_ok() { // get the user id from the execution result let id: i64 = execution.unwrap().id; // insert transaction to deduct user points - if db_transact::insert_transaction(transact_conn, db_transact::Transact { id: 0, user_id: id, trans_type: 8192, amount: -25, time: "".to_string() }).is_ok() { + if db_transact::insert_transaction( + transact_conn, + db_transact::Transact { + id: 0, + user_id: id, + trans_type: 8192, + amount: -25, + time: "".to_string(), + }, + ) + .is_ok() + { // deduct user points if db_auth::update_points(auth_conn, id, -25).is_ok() { // again, it doesn't really matter if points failed. send success anyways @@ -394,4 +447,4 @@ pub async fn delete_by_id(pool: &Pool, transact_pool: &Pool, auth_pool: &Pool, p }) .await? .map_err(error::ErrorInternalServerError::) -} \ No newline at end of file +} diff --git a/src/db_transact.rs b/src/db_transact.rs index a1a952bc..674d8d42 100644 --- a/src/db_transact.rs +++ b/src/db_transact.rs @@ -1,5 +1,5 @@ use actix_web::{error, web, Error}; -use rusqlite::{Statement, params}; +use rusqlite::{params, Statement}; use serde::Serialize; use crate::db_auth; @@ -11,10 +11,9 @@ pub struct Transact { pub user_id: i64, pub trans_type: i64, pub amount: i64, - pub time: String + pub time: String, } - pub type Pool = r2d2::Pool; pub type Connection = r2d2::PooledConnection; type TransactQuery = Result, rusqlite::Error>; @@ -23,17 +22,19 @@ pub enum TransactData { GetUserTransactions, } -pub async fn execute(pool: &Pool, query: TransactData, user: db_auth::User) -> Result, Error> { +pub async fn execute( + pool: &Pool, + query: TransactData, + user: db_auth::User, +) -> Result, Error> { let pool = pool.clone(); let conn = web::block(move || pool.get()) .await? .map_err(error::ErrorInternalServerError)?; - web::block(move || { - match query { - TransactData::GetUserTransactions => get_user_transact(conn, user), - } + web::block(move || match query { + TransactData::GetUserTransactions => get_user_transact(conn, user), }) .await? .map_err(error::ErrorInternalServerError) @@ -58,8 +59,14 @@ fn get_transact_rows(mut statement: Statement, user: db_auth::User) -> TransactQ .and_then(Iterator::collect) } -pub fn insert_transaction(conn: Connection, data: Transact) -> Result { - let mut stmt = conn.prepare("INSERT INTO transactions (user_id, trans_type, amount) VALUES (?, ?, ?);")?; +pub fn insert_transaction( + conn: Connection, + data: Transact, +) -> Result { + let mut stmt = + conn.prepare("INSERT INTO transactions (user_id, trans_type, amount) VALUES (?, ?, ?);")?; stmt.execute(params![data.user_id, data.trans_type, data.amount])?; - Ok(db_main::Id { id: conn.last_insert_rowid() }) -} \ No newline at end of file + Ok(db_main::Id { + id: conn.last_insert_rowid(), + }) +} diff --git a/src/forward.rs b/src/forward.rs index fe376cfb..fa267a6b 100644 --- a/src/forward.rs +++ b/src/forward.rs @@ -1,7 +1,10 @@ -use std::path::PathBuf; use actix_web::{web, HttpRequest, HttpResponse}; +use std::path::PathBuf; -pub async fn forward_frc_api_event_teams(_req: HttpRequest, path: web::Path<(String, String)>) -> HttpResponse { +pub async fn forward_frc_api_event_teams( + _req: HttpRequest, + path: web::Path<(String, String)>, +) -> HttpResponse { let (season, event) = path.into_inner(); let path: PathBuf = PathBuf::from(format!("cache/frc_api/{}/{}/teams.json", season, event)); if let Ok(content) = std::fs::read_to_string(&path) { @@ -14,7 +17,10 @@ pub async fn forward_frc_api_event_teams(_req: HttpRequest, path: web::Path<(Str } } -pub async fn forward_frc_api_event_matches(_req: HttpRequest, path: web::Path<(String, String)>) -> HttpResponse { +pub async fn forward_frc_api_event_matches( + _req: HttpRequest, + path: web::Path<(String, String)>, +) -> HttpResponse { let (season, event) = path.into_inner(); let path: PathBuf = PathBuf::from(format!("cache/frc_api/{}/{}/matches.json", season, event)); if let Ok(content) = std::fs::read_to_string(&path) { diff --git a/src/game_api.rs b/src/game_api.rs index dde64bdc..c07f98f7 100644 --- a/src/game_api.rs +++ b/src/game_api.rs @@ -1,7 +1,7 @@ use actix_web::{error, web, Error}; use rand::seq::SliceRandom; use rusqlite::Statement; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use crate::db_auth; use crate::db_main; @@ -13,7 +13,7 @@ pub struct DataStats { pub median: i64, pub third: i64, pub mean: i64, - pub decaying: i64 + pub decaying: i64, } #[derive(Serialize, Deserialize)] @@ -32,7 +32,7 @@ pub struct ClientInfo { username: String, team: i64, score: i64, - game_data: GameUserData + game_data: GameUserData, } #[derive(Serialize, Deserialize, Clone)] @@ -62,9 +62,12 @@ pub struct FrcApiTeams { pub teams: Vec, } -pub async fn get_owned_cards(pool: &db_auth::Pool, user: db_auth::User) -> Result { +pub async fn get_owned_cards( + pool: &db_auth::Pool, + user: db_auth::User, +) -> Result { let user_updated = db_auth::get_user_id(pool, user.id.to_string()).await?; - if user_updated.data == "" { + if user_updated.data == "" { db_auth::update_user_data( pool, user.id, @@ -74,33 +77,53 @@ pub async fn get_owned_cards(pool: &db_auth::Pool, user: db_auth::User) -> Resul wins: 0, losses: 0, ties: 0, - box_count: 0 - }).unwrap_or("".to_string()) - ).await?; + box_count: 0, + }) + .unwrap_or("".to_string()), + ) + .await?; Ok(ClientInfo { id: -1, username: "none".to_string(), team: -1, score: -1, - game_data: GameUserData { cards: vec![99999, 99998, 99997], hand: vec![99999, 99998, 99997], wins: 0, losses: 0, ties: 0, box_count: 0 } + game_data: GameUserData { + cards: vec![99999, 99998, 99997], + hand: vec![99999, 99998, 99997], + wins: 0, + losses: 0, + ties: 0, + box_count: 0, + }, }) } else { - Ok( - ClientInfo { - id: user_updated.id, - username: user_updated.username, - team: user_updated.team, - score: user_updated.score, - game_data: serde_json::from_str::(&user_updated.data).unwrap_or(GameUserData { cards: vec![99999, 99998, 99997], hand: vec![99999, 99998, 99997], wins: 0, losses: 0, ties: 0, box_count: 0 }) - } - ) + Ok(ClientInfo { + id: user_updated.id, + username: user_updated.username, + team: user_updated.team, + score: user_updated.score, + game_data: serde_json::from_str::(&user_updated.data).unwrap_or( + GameUserData { + cards: vec![99999, 99998, 99997], + hand: vec![99999, 99998, 99997], + wins: 0, + losses: 0, + ties: 0, + box_count: 0, + }, + ), + }) } } -pub async fn open_loot_box(auth_pool: &db_auth::Pool, main_pool: &db_main::Pool, user_param: db_auth::User) -> Result { +pub async fn open_loot_box( + auth_pool: &db_auth::Pool, + main_pool: &db_main::Pool, + user_param: db_auth::User, +) -> Result { let user_queried = db_auth::get_user_id(auth_pool, user_param.id.to_string()).await; if !user_queried.is_ok() { - return Ok(-1) + return Ok(-1); } let user = user_queried.unwrap(); let teams = db_main::get_team_numbers(main_pool, "2024".to_string()).await; @@ -112,13 +135,32 @@ pub async fn open_loot_box(auth_pool: &db_auth::Pool, main_pool: &db_main::Pool, let card = team_list.choose(&mut rand::thread_rng()).unwrap().clone(); if user.score >= 100 { if user.data == "" { - db_auth::update_user_data(auth_pool, user.id, serde_json::to_string(&GameUserData { cards: vec![99999, 99998, 99997], hand: vec![99999, 99998, 99997], wins: 0, losses: 0, ties: 0, box_count: 0 }).unwrap_or("".to_string())).await?; + db_auth::update_user_data( + auth_pool, + user.id, + serde_json::to_string(&GameUserData { + cards: vec![99999, 99998, 99997], + hand: vec![99999, 99998, 99997], + wins: 0, + losses: 0, + ties: 0, + box_count: 0, + }) + .unwrap_or("".to_string()), + ) + .await?; } - let mut current_user_data = serde_json::from_str::(&user.data).unwrap(); + let mut current_user_data = + serde_json::from_str::(&user.data).unwrap(); current_user_data.box_count += 1; current_user_data.cards.push(card); - db_auth::update_user_data(auth_pool, user.id, serde_json::to_string(¤t_user_data).unwrap_or("".to_string())).await?; + db_auth::update_user_data( + auth_pool, + user.id, + serde_json::to_string(¤t_user_data).unwrap_or("".to_string()), + ) + .await?; let auth_pool = auth_pool.clone(); let auth_conn = web::block(move || auth_pool.get()) @@ -140,13 +182,17 @@ pub async fn open_loot_box(auth_pool: &db_auth::Pool, main_pool: &db_main::Pool, #[derive(Serialize, Deserialize)] pub struct CardsPostData { - cards: Vec + cards: Vec, } -pub async fn set_held_cards(auth_pool: &db_auth::Pool, user_param: db_auth::User, data: &web::Json) -> Result { +pub async fn set_held_cards( + auth_pool: &db_auth::Pool, + user_param: db_auth::User, + data: &web::Json, +) -> Result { let user_queried = db_auth::get_user_id(auth_pool, user_param.id.to_string()).await; if !user_queried.is_ok() { - return Ok(CardsPostData { cards: vec![-1, 3] }) + return Ok(CardsPostData { cards: vec![-1, 3] }); } let user = user_queried.unwrap(); if user.data == "" { @@ -165,10 +211,17 @@ pub async fn set_held_cards(auth_pool: &db_auth::Pool, user_param: db_auth::User } if !cards_not_ok { current_user_data.hand = data.cards.clone(); - db_auth::update_user_data(auth_pool, user.id, serde_json::to_string(¤t_user_data).unwrap_or("".to_string())).await?; - return Ok(CardsPostData { cards: current_user_data.hand }) + db_auth::update_user_data( + auth_pool, + user.id, + serde_json::to_string(¤t_user_data).unwrap_or("".to_string()), + ) + .await?; + return Ok(CardsPostData { + cards: current_user_data.hand, + }); } else { - return Ok(CardsPostData { cards: vec![-1, 3] }) + return Ok(CardsPostData { cards: vec![-1, 3] }); } } } @@ -208,42 +261,52 @@ struct TeamDataset { auto_preload: Vec, auto_wing: Vec, auto_center: Vec, - auto_scores: Vec + auto_scores: Vec, } struct MainAnalysis { analysis: String, } -pub async fn execute(pool: &db_main::Pool, season: String, event: String, team: String) -> Result { +pub async fn execute( + pool: &db_main::Pool, + season: String, + event: String, + team: String, +) -> Result { let pool = pool.clone(); let conn = web::block(move || pool.get()) .await? .map_err(error::ErrorInternalServerError)?; - web::block(move || { - get_team(conn, season, event, team) - }) - .await? - .map_err(error::ErrorInternalServerError) + web::block(move || get_team(conn, season, event, team)) + .await? + .map_err(error::ErrorInternalServerError) } -fn get_team(conn: db_main::Connection, season: String, event: String, team: String) -> Result { - let stmt = conn.prepare("SELECT analysis FROM main WHERE season=:season AND event=:event AND team=:team;")?; +fn get_team( + conn: db_main::Connection, + season: String, + event: String, + team: String, +) -> Result { + let stmt = conn.prepare( + "SELECT analysis FROM main WHERE season=:season AND event=:event AND team=:team;", + )?; get_rows(stmt, [season, event, team]) } fn get_rows(mut statement: Statement, params: [String; 3]) -> Result { let data: Vec = statement .query_map(params.clone(), |row| { - Ok(MainAnalysis { - analysis: row.get(0)? + Ok(MainAnalysis { + analysis: row.get(0)?, }) }) .and_then(Iterator::collect) .unwrap(); - + let mut data_arr: TeamDataset = TeamDataset { trap_note: Vec::new(), climb: Vec::new(), @@ -258,25 +321,49 @@ fn get_rows(mut statement: Statement, params: [String; 3]) -> Result = entry.analysis.split(",").map(|v| v.parse::().unwrap_or(0)).collect(); - data_arr.trap_note.push(game_data.get(0).unwrap_or(&0).clone()); + let game_data: Vec = entry + .analysis + .split(",") + .map(|v| v.parse::().unwrap_or(0)) + .collect(); + data_arr + .trap_note + .push(game_data.get(0).unwrap_or(&0).clone()); data_arr.climb.push(game_data.get(1).unwrap_or(&0).clone()); - data_arr.buddy_climb.push(game_data.get(2).unwrap_or(&0).clone()); + data_arr + .buddy_climb + .push(game_data.get(2).unwrap_or(&0).clone()); data_arr.intake.push(game_data.get(3).unwrap_or(&0).clone()); data_arr.travel.push(game_data.get(4).unwrap_or(&0).clone()); - data_arr.outtake.push(game_data.get(5).unwrap_or(&0).clone()); - data_arr.speaker.push(game_data.get(6).unwrap_or(&0).clone()); - data_arr.amplifier.push(game_data.get(7).unwrap_or(&0).clone()); - data_arr.shots.push(game_data.get(6).unwrap_or(&0).clone() + game_data.get(7).unwrap_or(&0).clone()); + data_arr + .outtake + .push(game_data.get(5).unwrap_or(&0).clone()); + data_arr + .speaker + .push(game_data.get(6).unwrap_or(&0).clone()); + data_arr + .amplifier + .push(game_data.get(7).unwrap_or(&0).clone()); + data_arr + .shots + .push(game_data.get(6).unwrap_or(&0).clone() + game_data.get(7).unwrap_or(&0).clone()); data_arr.points.push(game_data.get(8).unwrap_or(&0).clone()); - data_arr.auto_preload.push(game_data.get(9).unwrap_or(&0).clone()); - data_arr.auto_wing.push(game_data.get(10).unwrap_or(&0).clone()); - data_arr.auto_center.push(game_data.get(11).unwrap_or(&0).clone()); - data_arr.auto_scores.push(game_data.get(12).unwrap_or(&0).clone()); + data_arr + .auto_preload + .push(game_data.get(9).unwrap_or(&0).clone()); + data_arr + .auto_wing + .push(game_data.get(10).unwrap_or(&0).clone()); + data_arr + .auto_center + .push(game_data.get(11).unwrap_or(&0).clone()); + data_arr + .auto_scores + .push(game_data.get(12).unwrap_or(&0).clone()); }); let intake_qrt = stats::quartiles_i64(&data_arr.intake); @@ -307,83 +394,84 @@ fn get_rows(mut statement: Statement, params: [String; 3]) -> Result().unwrap_or(0), trap_note: data_arr.trap_note.iter().sum::() as f64 / data_arr.trap_note.len() as f64, climb: data_arr.climb.iter().sum::() as f64 / data_arr.climb.len() as f64, - buddy_climb: data_arr.buddy_climb.iter().sum::() as f64 / data_arr.buddy_climb.len() as f64, + buddy_climb: data_arr.buddy_climb.iter().sum::() as f64 + / data_arr.buddy_climb.len() as f64, intake: DataStats { first: intake_qrt[0], median: intake_qrt[1], third: intake_qrt[2], mean: intake_means[0], - decaying: intake_means[1] + decaying: intake_means[1], }, travel: DataStats { first: travel_qrt[0], median: travel_qrt[1], third: travel_qrt[2], mean: travel_means[0], - decaying: travel_means[1] + decaying: travel_means[1], }, outtake: DataStats { first: outtake_qrt[0], median: outtake_qrt[1], third: outtake_qrt[2], mean: outtake_means[0], - decaying: outtake_means[1] + decaying: outtake_means[1], }, speaker: DataStats { first: speaker_qrt[0], median: speaker_qrt[1], third: speaker_qrt[2], mean: speaker_means[0], - decaying: speaker_means[1] + decaying: speaker_means[1], }, amplifier: DataStats { first: amplifier_qrt[0], median: amplifier_qrt[1], third: amplifier_qrt[2], mean: amplifier_means[0], - decaying: amplifier_means[1] + decaying: amplifier_means[1], }, total: DataStats { first: total_qrt[0], median: total_qrt[1], third: total_qrt[2], mean: total_means[0], - decaying: total_means[1] + decaying: total_means[1], }, points: DataStats { first: points_qrt[0], median: points_qrt[1], third: points_qrt[2], mean: points_means[0], - decaying: points_means[1] + decaying: points_means[1], }, auto_preload: DataStats { first: auto_preload_qrt[0], median: auto_preload_qrt[1], third: auto_preload_qrt[2], mean: auto_preload_means[0], - decaying: auto_preload_means[1] + decaying: auto_preload_means[1], }, auto_wing: DataStats { first: auto_wing_qrt[0], median: auto_wing_qrt[1], third: auto_wing_qrt[2], mean: auto_wing_means[0], - decaying: auto_wing_means[1] + decaying: auto_wing_means[1], }, auto_center: DataStats { first: auto_center_qrt[0], median: auto_center_qrt[1], third: auto_center_qrt[2], mean: auto_center_means[0], - decaying: auto_center_means[1] + decaying: auto_center_means[1], }, auto_scores: DataStats { first: auto_scores_qrt[0], median: auto_scores_qrt[1], third: auto_scores_qrt[2], mean: auto_scores_means[0], - decaying: auto_scores_means[1] + decaying: auto_scores_means[1], }, }) } diff --git a/src/main.rs b/src/main.rs index cef70a0c..8b3c7a82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,25 @@ -use std::{env, io, collections::HashMap, fs, pin::Pin, sync::RwLock}; use actix_governor::{Governor, GovernorConfigBuilder}; use actix_http::StatusCode; use actix_identity::{CookieIdentityPolicy, Identity, IdentityService}; -use actix_session::{SessionMiddleware, Session, config::PersistentSession}; -use actix_web::{error, middleware::{self, DefaultHeaders}, web, App, Error as AWError, HttpRequest, HttpResponse, HttpServer, cookie::Key, Responder, FromRequest, dev::Payload, http::header::ContentType}; +use actix_session::{config::PersistentSession, Session, SessionMiddleware}; +use actix_web::{ + cookie::Key, + dev::Payload, + error, + http::header::ContentType, + middleware::{self, DefaultHeaders}, + web, App, Error as AWError, FromRequest, HttpRequest, HttpResponse, HttpServer, Responder, +}; use actix_web_static_files::ResourceFiles; use dotenv::dotenv; -use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; +use openssl::{ + ssl::{SslAcceptor, SslFiletype, SslMethod}, + x509::X509, +}; use r2d2_sqlite::{self, SqliteConnectionManager}; use reqwest::Client; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, env, fs, io, pin::Pin, sync::RwLock}; use webauthn_rs::prelude::*; mod analyze; @@ -29,7 +39,7 @@ mod stats; // hashmap containing user session IDs #[derive(Serialize, Deserialize, Default, Clone)] struct Sessions { - user_map: HashMap + user_map: HashMap, } // gets a user object from requests. needed for db_auth::User param in handlers @@ -41,16 +51,20 @@ impl FromRequest for db_auth::User { let fut = Identity::from_request(req, payload); let session: Option<&web::Data>> = req.app_data(); if session.is_none() { - return Box::pin( - async { - Err(error::ErrorUnauthorized("{\"status\": \"unauthorized\"}")) - } - ); + return Box::pin(async { + Err(error::ErrorUnauthorized("{\"status\": \"unauthorized\"}")) + }); } let session = session.unwrap().clone(); Box::pin(async move { if let Some(identity) = fut.await?.identity() { - if let Some(user) = session.read().unwrap().user_map.get(&identity).map(|x| x.clone()) { + if let Some(user) = session + .read() + .unwrap() + .user_map + .get(&identity) + .map(|x| x.clone()) + { return Ok(user); } }; @@ -63,7 +77,7 @@ impl FromRequest for db_auth::User { struct Databases { main: db_main::Pool, auth: db_auth::Pool, - transact: db_transact::Pool + transact: db_transact::Pool, } // create secret key. probably could/should be an environment variable @@ -81,209 +95,269 @@ fn unauthorized_response() -> HttpResponse { // pong!! async fn misc_ping() -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .body("pong") - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .body("pong")) } // system is ok async fn debug_ok() -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .body("true") - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .body("true")) } // create account endpoint -async fn auth_post_create(db: web::Data, data: web::Json) -> impl Responder { +async fn auth_post_create( + db: web::Data, + data: web::Json, +) -> impl Responder { auth::create_account(&db.auth, data).await } // login endpoint -async fn auth_post_login(db: web::Data, session: web::Data>, identity: Identity, data: web::Json) -> impl Responder { +async fn auth_post_login( + db: web::Data, + session: web::Data>, + identity: Identity, + data: web::Json, +) -> impl Responder { auth::login(&db.auth, session, identity, data).await } // delete account endpoint required for apple platforms -async fn auth_post_delete(db: web::Data, data: web::Json) -> Result { - Ok( - auth::delete_account(&db.auth, data).await? - ) +async fn auth_post_delete( + db: web::Data, + data: web::Json, +) -> Result { + Ok(auth::delete_account(&db.auth, data).await?) } // destroy session endpoint -async fn auth_get_logout(session: web::Data>, identity: Identity) -> impl Responder { +async fn auth_get_logout( + session: web::Data>, + identity: Identity, +) -> impl Responder { auth::logout(session, identity).await } // create passkey -async fn auth_psk_create_start(db: web::Data, user: db_auth::User, session: Session, webauthn: web::Data) -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(passkey::webauthn_start_registration(&db.auth, user, session, webauthn).await?) - ) +async fn auth_psk_create_start( + db: web::Data, + user: db_auth::User, + session: Session, + webauthn: web::Data, +) -> Result { + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(passkey::webauthn_start_registration(&db.auth, user, session, webauthn).await?)) } // finish passkey creation -async fn auth_psk_create_finish(db: web::Data, user: db_auth::User, data: web::Json, session: Session, webauthn: web::Data) -> Result { - Ok( - passkey::webauthn_finish_registration(&db.auth, user, data, session, webauthn).await? - ) +async fn auth_psk_create_finish( + db: web::Data, + user: db_auth::User, + data: web::Json, + session: Session, + webauthn: web::Data, +) -> Result { + Ok(passkey::webauthn_finish_registration(&db.auth, user, data, session, webauthn).await?) } // get passkey auth challenge -async fn auth_psk_auth_start(db: web::Data, username: web::Path, session: Session, webauthn: web::Data) -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(passkey::webauthn_start_authentication(&db.auth, username.into_inner(), session, webauthn).await?) - ) +async fn auth_psk_auth_start( + db: web::Data, + username: web::Path, + session: Session, + webauthn: web::Data, +) -> Result { + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json( + passkey::webauthn_start_authentication( + &db.auth, + username.into_inner(), + session, + webauthn, + ) + .await?, + )) } // finish passkey authentication -async fn auth_psk_auth_finish(db: web::Data, cred: web::Json, session: Session, identity: Identity, webauthn: web::Data, sessions: web::Data>) -> Result { - Ok( - passkey::webauthn_finish_authentication(&db.auth, cred, session, identity, webauthn, sessions).await? +async fn auth_psk_auth_finish( + db: web::Data, + cred: web::Json, + session: Session, + identity: Identity, + webauthn: web::Data, + sessions: web::Data>, +) -> Result { + Ok(passkey::webauthn_finish_authentication( + &db.auth, cred, session, identity, webauthn, sessions, ) + .await?) } #[derive(Serialize, Deserialize)] pub struct DataMeta { pub seasons: Vec, pub events: Vec, - pub teams: Vec + pub teams: Vec, } // valid entries metadata. iOS and web clients load from this. async fn data_get_meta() -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(DataMeta { - seasons: env::var("SEASONS").unwrap_or_else(|_| "0".to_string()).split(",").map(|s| s.to_string()).collect::>(), - events: env::var("EVENTS").unwrap_or_else(|_| "0".to_string()).split(",").map(|s| s.to_string()).collect::>(), - teams: env::var("TEAMS").unwrap_or_else(|_| "0".to_string()).split(",").map(|s| s.to_string()).collect::>() - }) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(DataMeta { + seasons: env::var("SEASONS") + .unwrap_or_else(|_| "0".to_string()) + .split(",") + .map(|s| s.to_string()) + .collect::>(), + events: env::var("EVENTS") + .unwrap_or_else(|_| "0".to_string()) + .split(",") + .map(|s| s.to_string()) + .collect::>(), + teams: env::var("TEAMS") + .unwrap_or_else(|_| "0".to_string()) + .split(",") + .map(|s| s.to_string()) + .collect::>(), + })) } // access denied template fn access_denied_team() -> HttpResponse { - HttpResponse::Unauthorized() - .body("you must be affiliated with a valid team to access data") + HttpResponse::Unauthorized().body("you must be affiliated with a valid team to access data") } // get detailed data by submission id. used in /detail -async fn data_get_detailed(path: web::Path, db: web::Data, user: db_auth::User) -> Result { +async fn data_get_detailed( + path: web::Path, + db: web::Data, + user: db_auth::User, +) -> Result { if user.team != 0 { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_main::execute(&db.main, db_main::MainData::GetDataDetailed, path).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(db_main::execute(&db.main, db_main::MainData::GetDataDetailed, path).await?)) } else { Ok(access_denied_team()) } } // check if a submission exists, by id. used in submit script to verify submission (verification is mostly a gimmick but whatever) -async fn data_get_exists(path: web::Path, db: web::Data, user: db_auth::User) -> Result { +async fn data_get_exists( + path: web::Path, + db: web::Data, + user: db_auth::User, +) -> Result { if user.team != 0 { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_main::execute(&db.main, db_main::MainData::DataExists, path).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(db_main::execute(&db.main, db_main::MainData::DataExists, path).await?)) } else { Ok(access_denied_team()) } } // get summary of all data for a given team at an event in a season. used on /browse -async fn data_get_main_brief_team(path: web::Path, db: web::Data, user: db_auth::User) -> Result { +async fn data_get_main_brief_team( + path: web::Path, + db: web::Data, + user: db_auth::User, +) -> Result { if user.team != 0 { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_main::execute(&db.main, db_main::MainData::BriefTeam, path).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(db_main::execute(&db.main, db_main::MainData::BriefTeam, path).await?)) } else { Ok(access_denied_team()) } } // get summary of all data for a given match at an event, in a specified season. used on /browsw -async fn data_get_main_brief_match(path: web::Path, db: web::Data, user: db_auth::User) -> Result { +async fn data_get_main_brief_match( + path: web::Path, + db: web::Data, + user: db_auth::User, +) -> Result { if user.team != 0 { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_main::execute(&db.main, db_main::MainData::BriefMatch, path).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(db_main::execute(&db.main, db_main::MainData::BriefMatch, path).await?)) } else { Ok(access_denied_team()) } } // get summary of all data from an event, given a season. used for /browse -async fn data_get_main_brief_event(path: web::Path, db: web::Data, user: db_auth::User) -> Result { +async fn data_get_main_brief_event( + path: web::Path, + db: web::Data, + user: db_auth::User, +) -> Result { if user.team != 0 { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_main::execute(&db.main, db_main::MainData::BriefEvent, path).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(db_main::execute(&db.main, db_main::MainData::BriefEvent, path).await?)) } else { Ok(access_denied_team()) } } -async fn data_get_main_brief_season(path: web::Path, db: web::Data, user: db_auth::User) -> Result { +async fn data_get_main_brief_season( + path: web::Path, + db: web::Data, + user: db_auth::User, +) -> Result { if user.team != 0 { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_main::execute(&db.main, db_main::MainData::BriefSeason, path).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(db_main::execute(&db.main, db_main::MainData::BriefSeason, path).await?)) } else { Ok(access_denied_team()) } } // get summary of all submissions created by a certain user id. used for /browse -async fn data_get_main_brief_user(path: web::Path, db: web::Data, user: db_auth::User) -> Result { +async fn data_get_main_brief_user( + path: web::Path, + db: web::Data, + user: db_auth::User, +) -> Result { if user.team != 0 { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_main::execute(&db.main, db_main::MainData::BriefUser, path).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(db_main::execute(&db.main, db_main::MainData::BriefUser, path).await?)) } else { Ok(access_denied_team()) } } // get basic data about all teams at an event, in a season. used for event rankings. ** NO AUTH ** -async fn data_get_main_teams(path: web::Path, db: web::Data) -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_main::execute(&db.main, db_main::MainData::GetTeams, path).await?) - ) +async fn data_get_main_teams( + path: web::Path, + db: web::Data, +) -> Result { + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(db_main::execute(&db.main, db_main::MainData::GetTeams, path).await?)) } // get POSTed data from form -async fn data_post_submit(data: web::Json, db: web::Data, user: db_auth::User) -> Result { +async fn data_post_submit( + data: web::Json, + db: web::Data, + user: db_auth::User, +) -> Result { if user.team != 0 { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_main::execute_insert(&db.main, &db.transact, &db.auth, data, user).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(db_main::execute_insert(&db.main, &db.transact, &db.auth, data, user).await?)) } else { Ok(access_denied_team()) } @@ -295,101 +369,167 @@ async fn event_get_frc_api(req: HttpRequest, path: web::Path<(String, String)>) } // forward frc api data for events. used on main form to ensure entered matches and teams are valid -async fn event_get_frc_api_matches(req: HttpRequest, path: web::Path<(String, String)>/*, user: db_auth::User*/) -> HttpResponse { +async fn event_get_frc_api_matches( + req: HttpRequest, + path: web::Path<(String, String)>, /*, user: db_auth::User*/ +) -> HttpResponse { // if user.team != 0 { - forward::forward_frc_api_event_matches(req, path).await + forward::forward_frc_api_event_matches(req, path).await // } else { // access_denied_team() // } } // get all valid submission IDs. used on /manage to create list of IDs that can be acted on -async fn manage_get_submission_ids(path: web::Path, db: web::Data, user: db_auth::User) -> Result { +async fn manage_get_submission_ids( + path: web::Path, + db: web::Data, + user: db_auth::User, +) -> Result { if user.admin == "true" { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_main::execute(&db.main, db_main::MainData::Id, path).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(db_main::execute(&db.main, db_main::MainData::Id, path).await?)) } else { Ok(unauthorized_response()) } } // gets list of all valid user ids, used in /manageScouts -async fn manage_get_all_users(db: web::Data, user: db_auth::User) -> Result { +async fn manage_get_all_users( + db: web::Data, + user: db_auth::User, +) -> Result { if user.admin == "true" { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_auth::execute_get_users_mgmt(&db.auth, db_auth::UserQueryType::All, user).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json( + db_auth::execute_get_users_mgmt(&db.auth, db_auth::UserQueryType::All, user) + .await?, + )) } else { Ok(unauthorized_response()) } } // gets list of users in a team, used in /manageTeam -async fn manage_get_all_users_in_team(db: web::Data, user: db_auth::User) -> Result { +async fn manage_get_all_users_in_team( + db: web::Data, + user: db_auth::User, +) -> Result { if user.admin == "true" || user.team_admin != 0 { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_auth::execute_get_users_mgmt(&db.auth, db_auth::UserQueryType::Team, user).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json( + db_auth::execute_get_users_mgmt(&db.auth, db_auth::UserQueryType::Team, user) + .await?, + )) } else { Ok(unauthorized_response()) } } // gets all access keys, used for /manageTeams -async fn manage_get_all_keys(db: web::Data, user: db_auth::User) -> Result { +async fn manage_get_all_keys( + db: web::Data, + user: db_auth::User, +) -> Result { if user.admin == "true" { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_auth::get_access_key(&db.auth, "".to_string(), db_auth::AccessKeyQuery::AllKeys).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json( + db_auth::get_access_key(&db.auth, "".to_string(), db_auth::AccessKeyQuery::AllKeys) + .await?, + )) + } else { + Ok(unauthorized_response()) + } +} + +// data dump +async fn manage_data_dump( + db: web::Data, + user: db_auth::User, + path: web::Path, +) -> Result { + if user.admin == "true" { + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(db_main::execute(&db.main, db_main::MainData::GetAllData, path).await?)) } else { Ok(unauthorized_response()) } } // DELETE endpoint to remove a submission. used in /manage -async fn manage_delete_submission(db: web::Data, user: db_auth::User, path: web::Path) -> Result { +async fn manage_delete_submission( + db: web::Data, + user: db_auth::User, + path: web::Path, +) -> Result { if user.admin == "true" { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .body(db_main::delete_by_id(&db.main, &db.transact, &db.auth, path).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .body(db_main::delete_by_id(&db.main, &db.transact, &db.auth, path).await?)) } else { Ok(unauthorized_response()) } } // DELETE endpoint to remove a user, used in /manageScouts -async fn manage_delete_user(req: HttpRequest, db: web::Data, user: db_auth::User) -> Result { +async fn manage_delete_user( + req: HttpRequest, + db: web::Data, + user: db_auth::User, +) -> Result { if user.admin == "true" { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .body(db_auth::execute_manage_user(&db.auth, db_auth::UserManageAction::DeleteUser, [req.match_info().get("user_id").unwrap().parse().unwrap(), "".to_string()]).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .body( + db_auth::execute_manage_user( + &db.auth, + db_auth::UserManageAction::DeleteUser, + [ + req.match_info().get("user_id").unwrap().parse().unwrap(), + "".to_string(), + ], + ) + .await?, + )) } else { Ok(unauthorized_response()) } } // DELETE endpoint to remove a user, but for a team admin (requires that target user is member of team). used in /manageTeam -async fn manage_delete_user_team_admin(req: HttpRequest, db: web::Data, user: db_auth::User) -> Result { +async fn manage_delete_user_team_admin( + req: HttpRequest, + db: web::Data, + user: db_auth::User, +) -> Result { if user.admin == "true" || user.team_admin != 0 { - if user.admin == "true" || db_auth::get_user_id(&db.auth, req.match_info().get("user_id").unwrap().parse().unwrap()).await?.team == user.team_admin { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .body(db_auth::execute_manage_user(&db.auth, db_auth::UserManageAction::DeleteUser, [req.match_info().get("user_id").unwrap().parse().unwrap(), "".to_string()]).await?) + if user.admin == "true" + || db_auth::get_user_id( + &db.auth, + req.match_info().get("user_id").unwrap().parse().unwrap(), ) + .await? + .team + == user.team_admin + { + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .body( + db_auth::execute_manage_user( + &db.auth, + db_auth::UserManageAction::DeleteUser, + [ + req.match_info().get("user_id").unwrap().parse().unwrap(), + "".to_string(), + ], + ) + .await?, + )) } else { Ok(unauthorized_response()) } @@ -399,193 +539,262 @@ async fn manage_delete_user_team_admin(req: HttpRequest, db: web::Data, user: db_auth::User) -> Result { +async fn manage_delete_access_key( + req: HttpRequest, + db: web::Data, + user: db_auth::User, +) -> Result { if user.admin == "true" { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .body(db_auth::delete_access_key(&db.auth, req.match_info().get("access_key_id").unwrap().parse().unwrap()).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .body( + db_auth::delete_access_key( + &db.auth, + req.match_info() + .get("access_key_id") + .unwrap() + .parse() + .unwrap(), + ) + .await?, + )) } else { Ok(unauthorized_response()) } } // patch to update a user's administration status, used in /manageScouts -async fn manage_patch_admin(req: HttpRequest, db: web::Data, user: db_auth::User) -> Result { +async fn manage_patch_admin( + req: HttpRequest, + db: web::Data, + user: db_auth::User, +) -> Result { if user.admin == "true" { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .body(db_auth::execute_manage_user(&db.auth, db_auth::UserManageAction::ModifyAdmin, [req.match_info().get("admin").unwrap().parse().unwrap(), req.match_info().get("user_id").unwrap().parse().unwrap()]).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .body( + db_auth::execute_manage_user( + &db.auth, + db_auth::UserManageAction::ModifyAdmin, + [ + req.match_info().get("admin").unwrap().parse().unwrap(), + req.match_info().get("user_id").unwrap().parse().unwrap(), + ], + ) + .await?, + )) } else { Ok(unauthorized_response()) } } // patch to update a user's [team] administration status, used in /manageScouts -async fn manage_patch_team_admin(req: HttpRequest, db: web::Data, user: db_auth::User) -> Result { +async fn manage_patch_team_admin( + req: HttpRequest, + db: web::Data, + user: db_auth::User, +) -> Result { if user.admin == "true" { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .body(db_auth::execute_manage_user(&db.auth, db_auth::UserManageAction::ModifyTeamAdmin, [req.match_info().get("admin").unwrap().parse().unwrap(), req.match_info().get("user_id").unwrap().parse().unwrap()]).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .body( + db_auth::execute_manage_user( + &db.auth, + db_auth::UserManageAction::ModifyTeamAdmin, + [ + req.match_info().get("admin").unwrap().parse().unwrap(), + req.match_info().get("user_id").unwrap().parse().unwrap(), + ], + ) + .await?, + )) } else { Ok(unauthorized_response()) } } // patch to update a user's points, used in /manageScouts -async fn manage_patch_points(req: HttpRequest, db: web::Data, user: db_auth::User) -> Result { +async fn manage_patch_points( + req: HttpRequest, + db: web::Data, + user: db_auth::User, +) -> Result { if user.admin == "true" { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .body(db_auth::execute_manage_user(&db.auth, db_auth::UserManageAction::ModifyPoints, [req.match_info().get("modify").unwrap().parse().unwrap(), req.match_info().get("user_id").unwrap().parse().unwrap()]).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .body( + db_auth::execute_manage_user( + &db.auth, + db_auth::UserManageAction::ModifyPoints, + [ + req.match_info().get("modify").unwrap().parse().unwrap(), + req.match_info().get("user_id").unwrap().parse().unwrap(), + ], + ) + .await?, + )) } else { Ok(unauthorized_response()) } } // patch to modify an existing access key, used in /manageTeams -async fn manage_patch_access_key(req: HttpRequest, db: web::Data, user: db_auth::User) -> Result { +async fn manage_patch_access_key( + req: HttpRequest, + db: web::Data, + user: db_auth::User, +) -> Result { if user.admin == "true" { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .body(db_auth::update_access_key(&db.auth, req.match_info().get("key").unwrap().parse().unwrap(), req.match_info().get("id").unwrap().parse().unwrap()).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .body( + db_auth::update_access_key( + &db.auth, + req.match_info().get("key").unwrap().parse().unwrap(), + req.match_info().get("id").unwrap().parse().unwrap(), + ) + .await?, + )) } else { Ok(unauthorized_response()) } } // post to create a new access key, used in /manageTeams -async fn manage_post_access_key(req: HttpRequest, db: web::Data, user: db_auth::User) -> Result { +async fn manage_post_access_key( + req: HttpRequest, + db: web::Data, + user: db_auth::User, +) -> Result { if user.admin == "true" { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .body(db_auth::create_access_key(&db.auth, req.match_info().get("key").unwrap().parse().unwrap(), req.match_info().get("team").unwrap().parse().unwrap()).await?) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .body( + db_auth::create_access_key( + &db.auth, + req.match_info().get("key").unwrap().parse().unwrap(), + req.match_info().get("team").unwrap().parse().unwrap(), + ) + .await?, + )) } else { Ok(unauthorized_response()) } } // get transactions, used in /pointRecords -async fn misc_get_transact_me(db: web::Data, user: db_auth::User) -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_transact::execute(&db.transact, db_transact::TransactData::GetUserTransactions, user).await?) - ) +async fn misc_get_transact_me( + db: web::Data, + user: db_auth::User, +) -> Result { + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json( + db_transact::execute( + &db.transact, + db_transact::TransactData::GetUserTransactions, + user, + ) + .await?, + )) } // get to confirm session status and obtain current user id. used in main form to ensure session is active async fn misc_get_whoami(user: db_auth::User) -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_main::Id { id: user.id }) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(db_main::Id { id: user.id })) } // if you aren't D6MFYYVHA8 you may want to change this -const APPLE_APP_SITE_ASSOC: &str = "{\"webcredentials\":{\"apps\":[\"D6MFYYVHA8.com.jayagra.beartracks\",\"D6MFYYVHA8.com.jayagra.beartracks-scout\",\"D6MFYYVHA8.com.jayagra.beartracks-manage\"]}}"; +const APPLE_APP_SITE_ASSOC: &str = "{\"webcredentials\":{\"apps\":[\"D6MFYYVHA8.com.jayagra.beartracks\",\"D6MFYYVHA8.com.jayagra.beartracks-scout\",\"D6MFYYVHA8.com.jayagra.beartracks-manage\",\"D6MFYYVHA8.com.jayagra.beartracks.watchkitapp\"]}}"; async fn misc_apple_app_site_association() -> Result { - Ok( - HttpResponse::Ok() - .content_type(ContentType::json()) - .body(APPLE_APP_SITE_ASSOC) - ) + Ok(HttpResponse::Ok() + .content_type(ContentType::json()) + .body(APPLE_APP_SITE_ASSOC)) } // get all points. used to construct the leaderboard -async fn points_get_all(db: web::Data, _user: db_auth::User) -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(db_auth::execute_scores(&db.auth, db_auth::AuthData::GetUserScores).await?) - ) +async fn points_get_all( + db: web::Data, + _user: db_auth::User, +) -> Result { + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(db_auth::execute_scores(&db.auth, db_auth::AuthData::GetUserScores).await?)) } // get spin wheel for the casino -async fn casino_wheel(db: web::Data, user: db_auth::User) -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .body(casino::spin_thing(&db.auth, &db.transact, user).await?) - ) +async fn casino_wheel( + db: web::Data, + user: db_auth::User, +) -> Result { + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .body(casino::spin_thing(&db.auth, &db.transact, user).await?)) } // get for debugging. returns the current user object. async fn debug_get_user(user: db_auth::User) -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(user) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(user)) } // server health for debug async fn debug_health(session: web::Data>) -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json(server_health::get_server_health(session)) - ) + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(server_health::get_server_health(session))) } // get all user's owned cards -async fn game_get_cards(db: web::Data, user: db_auth::User) -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json( - game_api::get_owned_cards(&db.auth, user).await? - ) - ) +async fn game_get_cards( + db: web::Data, + user: db_auth::User, +) -> Result { + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(game_api::get_owned_cards(&db.auth, user).await?)) } // get random team from scouted teams -async fn game_open_lootbox(db: web::Data, user: db_auth::User) -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json( - game_api::open_loot_box(&db.auth, &db.main, user).await? - ) - ) +async fn game_open_lootbox( + db: web::Data, + user: db_auth::User, +) -> Result { + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(game_api::open_loot_box(&db.auth, &db.main, user).await?)) } // set player's hand -async fn game_set_hand(db: web::Data, data: web::Json, user: db_auth::User) -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json( - game_api::set_held_cards(&db.auth, user, &data).await? - ) - ) +async fn game_set_hand( + db: web::Data, + data: web::Json, + user: db_auth::User, +) -> Result { + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json(game_api::set_held_cards(&db.auth, user, &data).await?)) } -async fn game_get_team(req: HttpRequest, db: web::Data, _user: db_auth::User) -> Result { - Ok( - HttpResponse::Ok() - .insert_header(("Cache-Control", "no-cache")) - .json( - game_api::execute( - &db.main, - req.match_info().get("season").unwrap().parse().unwrap(), - req.match_info().get("event").unwrap().parse().unwrap(), - req.match_info().get("team").unwrap().parse().unwrap(), - ).await? +async fn game_get_team( + req: HttpRequest, + db: web::Data, + _user: db_auth::User, +) -> Result { + Ok(HttpResponse::Ok() + .insert_header(("Cache-Control", "no-cache")) + .json( + game_api::execute( + &db.main, + req.match_info().get("season").unwrap().parse().unwrap(), + req.match_info().get("event").unwrap().parse().unwrap(), + req.match_info().get("team").unwrap().parse().unwrap(), ) - ) + .await?, + )) } include!(concat!(env!("OUT_DIR"), "/generated.rs")); @@ -604,54 +813,115 @@ async fn main() -> io::Result<()> { } // cache all possible files - let seasons = env::var("SEASONS").unwrap_or_else(|_| "0".to_string()).split(",").map(|s| s.to_string()).collect::>(); - let events = env::var("EVENTS").unwrap_or_else(|_| "0".to_string()).split(",").map(|s| s.to_string()).collect::>(); + let seasons = env::var("SEASONS") + .unwrap_or_else(|_| "0".to_string()) + .split(",") + .map(|s| s.to_string()) + .collect::>(); + let events = env::var("EVENTS") + .unwrap_or_else(|_| "0".to_string()) + .split(",") + .map(|s| s.to_string()) + .collect::>(); for i in 0..seasons.len() { for j in 0..events.len() { // cache team list - let team_target_url = format!("https://frc-api.firstinspires.org/v3.0/{}/teams?eventCode={}", seasons[i], events[j]); + let team_target_url = format!( + "https://frc-api.firstinspires.org/v3.0/{}/teams?eventCode={}", + seasons[i], events[j] + ); let team_client = Client::new(); let team_response = team_client - .request(actix_http::Method::GET, team_target_url) - .header("Authorization", format!("Basic {}", env::var("FRC_API_KEY").unwrap_or_else(|_| "NONE".to_string()))) - .send() - .await; - + .request(actix_http::Method::GET, team_target_url) + .header( + "Authorization", + format!( + "Basic {}", + env::var("FRC_API_KEY").unwrap_or_else(|_| "NONE".to_string()) + ), + ) + .send() + .await; + match team_response { Ok(response) => { if response.status() == 200 { fs::create_dir_all(format!("cache/frc_api/{}/{}", seasons[i], events[j]))?; - fs::write(format!("cache/frc_api/{}/{}/teams.json", seasons[i], events[j]), response.text().await.unwrap()).expect(format!("Failed to cache {}/{} team JSON. Could not write file.", seasons[i], events[j]).as_str()); + fs::write( + format!("cache/frc_api/{}/{}/teams.json", seasons[i], events[j]), + response.text().await.unwrap(), + ) + .expect( + format!( + "Failed to cache {}/{} team JSON. Could not write file.", + seasons[i], events[j] + ) + .as_str(), + ); } else { - log::error!("Failed to cache {}/{} team JSON. Response status {}.", seasons[i], events[j], response.status()); + log::error!( + "Failed to cache {}/{} team JSON. Response status {}.", + seasons[i], + events[j], + response.status() + ); } } Err(_) => { - log::error!("Failed to cache {}/{} team JSON. Response was not OK.", seasons[i], events[j]); - }, + log::error!( + "Failed to cache {}/{} team JSON. Response was not OK.", + seasons[i], + events[j] + ); + } } let match_target_url = format!("https://frc-api.firstinspires.org/v3.0/{}/schedule/{}?tournamentLevel=qualification", seasons[i], events[j]); let match_client = Client::new(); let match_response = match_client - .request(actix_http::Method::GET, match_target_url) - .header("Authorization", format!("Basic {}", env::var("FRC_API_KEY").unwrap_or_else(|_| "NONE".to_string()))) - .send() - .await; - + .request(actix_http::Method::GET, match_target_url) + .header( + "Authorization", + format!( + "Basic {}", + env::var("FRC_API_KEY").unwrap_or_else(|_| "NONE".to_string()) + ), + ) + .send() + .await; + match match_response { Ok(response) => { if response.status() == 200 { fs::create_dir_all(format!("cache/frc_api/{}/{}", seasons[i], events[j]))?; - fs::write(format!("cache/frc_api/{}/{}/matches.json", seasons[i], events[j]), response.text().await.unwrap()).expect(format!("Failed to cache {}/{} match JSON. Could not write file.", seasons[i], events[j]).as_str()); + fs::write( + format!("cache/frc_api/{}/{}/matches.json", seasons[i], events[j]), + response.text().await.unwrap(), + ) + .expect( + format!( + "Failed to cache {}/{} match JSON. Could not write file.", + seasons[i], events[j] + ) + .as_str(), + ); } else { - log::error!("Failed to cache {}/{} match JSON. Response status {}.", seasons[i], events[j], response.status()); + log::error!( + "Failed to cache {}/{} match JSON. Response status {}.", + seasons[i], + events[j], + response.status() + ); } } Err(_) => { - log::error!("Failed to cache {}/{} match JSON. Response was not OK.", seasons[i], events[j]); - }, + log::error!( + "Failed to cache {}/{} match JSON. Response was not OK.", + seasons[i], + events[j] + ); + } } } } @@ -665,21 +935,27 @@ async fn main() -> io::Result<()> { let main_db_manager = SqliteConnectionManager::file("data.db"); let main_db_pool = db_main::Pool::new(main_db_manager).unwrap(); let main_db_connection = main_db_pool.get().expect("main db: connection failed"); - main_db_connection.execute_batch("PRAGMA journal_mode=WAL;").expect("main db: WAL failed"); + main_db_connection + .execute_batch("PRAGMA journal_mode=WAL;") + .expect("main db: WAL failed"); drop(main_db_connection); // auth database connection let auth_db_manager = SqliteConnectionManager::file("data_auth.db"); let auth_db_pool = db_main::Pool::new(auth_db_manager).unwrap(); let auth_db_connection = auth_db_pool.get().expect("auth db: connection failed"); - auth_db_connection.execute_batch("PRAGMA journal_mode=WAL;").expect("auth db: WAL failed"); + auth_db_connection + .execute_batch("PRAGMA journal_mode=WAL;") + .expect("auth db: WAL failed"); drop(auth_db_connection); // transaction database connection let trans_db_manager = SqliteConnectionManager::file("data_transact.db"); let trans_db_pool = db_main::Pool::new(trans_db_manager).unwrap(); let trans_db_connection = trans_db_pool.get().expect("trans db: connection failed"); - trans_db_connection.execute_batch("PRAGMA journal_mode=WAL;").expect("trans db: WAL failed"); + trans_db_connection + .execute_batch("PRAGMA journal_mode=WAL;") + .expect("trans db: WAL failed"); drop(trans_db_connection); // create secret key for uh cookies i think @@ -699,8 +975,19 @@ async fn main() -> io::Result<()> { */ // create ssl builder for tls config let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); - builder.set_private_key_file("./ssl/key.pem", SslFiletype::PEM).unwrap(); - builder.set_certificate_chain_file("./ssl/cert.pem").unwrap(); + builder + .set_private_key_file("./ssl/key.pem", SslFiletype::PEM) + .unwrap(); + builder + .set_certificate_chain_file("./ssl/cert.pem") + .unwrap(); + let intermediate_cert_url = "https://letsencrypt.org/certs/lets-encrypt-r3.der"; + let intermediate_bytes = reqwest::blocking::get(intermediate_cert_url) + .unwrap() + .bytes() + .unwrap(); + let intermediate_cert = X509::from_der(&intermediate_bytes).unwrap(); + builder.add_extra_chain_cert(intermediate_cert).unwrap(); // config done. now, create the new HttpServer log::info!("[OK] starting bearTracks on port 443 and 80"); @@ -710,7 +997,11 @@ async fn main() -> io::Result<()> { let generated = generate(); App::new() // add databases to app data - .app_data(web::Data::new(Databases {main: main_db_pool.clone(), auth: auth_db_pool.clone(), transact: trans_db_pool.clone() })) + .app_data(web::Data::new(Databases { + main: main_db_pool.clone(), + auth: auth_db_pool.clone(), + transact: trans_db_pool.clone(), + })) // add sessions to app data .app_data(sessions.clone()) // add webauthn to app data @@ -734,108 +1025,243 @@ async fn main() -> io::Result<()> { .cookie_secure(false) .session_lifecycle( PersistentSession::default() - .session_ttl(actix_web::cookie::time::Duration::weeks(2)) + .session_ttl(actix_web::cookie::time::Duration::weeks(2)), ) - .build() + .build(), ) // default headers for caching. overridden on most all api endpoints - .wrap(DefaultHeaders::new().add(("Cache-Control", "public, max-age=23328000")).add(("X-bearTracks", "5.0.5"))) + .wrap( + DefaultHeaders::new() + .add(("Cache-Control", "public, max-age=23328000")) + .add(("X-bearTracks", "5.1.0")), + ) /* src endpoints */ - // GET individual files - .route("/", web::get().to(static_files::static_index)) - .route("/blackjack", web::get().to(static_files::static_blackjack)) - .route("/create", web::get().to(static_files::static_create)) - .route("/main", web::get().to(static_files::static_main)) - .route("/login", web::get().to(static_files::static_login)) - .route("/passkey", web::get().to(static_files::static_passkey)) - .route("/pointRecords", web::get().to(static_files::static_point_records)) - .route("/points", web::get().to(static_files::static_points)) - .route("/safari-pinned-tab.svg", web::get().to(static_files::static_safari_pinned)) - .route("/scouts", web::get().to(static_files::static_scouts)) - .route("/settings", web::get().to(static_files::static_settings)) - .route("/site.webmanifest", web::get().to(static_files::static_webmanifest)) - .route("/spin", web::get().to(static_files::static_spin)) - .route("/android-chrome-192x192.png", web::get().to(static_files::static_android_chrome_192)) - .route("/android-chrome-512x512.png", web::get().to(static_files::static_android_chrome_512)) - .route("/apple-touch-icon.png", web::get().to(static_files::static_apple_touch_icon)) - .route("/favicon-16x16.png", web::get().to(static_files::static_favicon_16)) - .route("/favicon-32x32.png", web::get().to(static_files::static_favicon_32)) - .route("/favicon.ico", web::get().to(static_files::static_favicon)) - // GET folders - .service(ResourceFiles::new("/static", generated)) + // GET individual files + .route("/", web::get().to(static_files::static_index)) + .route("/blackjack", web::get().to(static_files::static_blackjack)) + .route("/create", web::get().to(static_files::static_create)) + .route("/main", web::get().to(static_files::static_main)) + .route("/login", web::get().to(static_files::static_login)) + .route("/passkey", web::get().to(static_files::static_passkey)) + .route( + "/pointRecords", + web::get().to(static_files::static_point_records), + ) + .route("/points", web::get().to(static_files::static_points)) + .route( + "/safari-pinned-tab.svg", + web::get().to(static_files::static_safari_pinned), + ) + .route("/scouts", web::get().to(static_files::static_scouts)) + .route("/settings", web::get().to(static_files::static_settings)) + .route( + "/site.webmanifest", + web::get().to(static_files::static_webmanifest), + ) + .route("/spin", web::get().to(static_files::static_spin)) + .route( + "/android-chrome-192x192.png", + web::get().to(static_files::static_android_chrome_192), + ) + .route( + "/android-chrome-512x512.png", + web::get().to(static_files::static_android_chrome_512), + ) + .route( + "/apple-touch-icon.png", + web::get().to(static_files::static_apple_touch_icon), + ) + .route( + "/favicon-16x16.png", + web::get().to(static_files::static_favicon_16), + ) + .route( + "/favicon-32x32.png", + web::get().to(static_files::static_favicon_32), + ) + .route("/favicon.ico", web::get().to(static_files::static_favicon)) + // GET folders + .service(ResourceFiles::new("/static", generated)) /* auth endpoints */ - // GET - .service(web::resource("/logout").route(web::get().to(auth_get_logout))) - // POST - .service(web::resource("/api/v1/auth/create").route(web::post().to(auth_post_create))) - .service(web::resource("/api/v1/auth/login").route(web::post().to(auth_post_login))) - .service(web::resource("/api/v1/auth/delete").route(web::post().to(auth_post_delete))) - .service(web::resource("/api/v1/auth/passkey/register_start").route(web::post().to(auth_psk_create_start))) - .service(web::resource("/api/v1/auth/passkey/register_finish").route(web::post().to(auth_psk_create_finish))) - .service(web::resource("/api/v1/auth/passkey/auth_start/{username}").route(web::post().to(auth_psk_auth_start))) - .service(web::resource("/api/v1/auth/passkey/auth_finish").route(web::post().to(auth_psk_auth_finish))) + // GET + .service(web::resource("/logout").route(web::get().to(auth_get_logout))) + // POST + .service(web::resource("/api/v1/auth/create").route(web::post().to(auth_post_create))) + .service(web::resource("/api/v1/auth/login").route(web::post().to(auth_post_login))) + .service(web::resource("/api/v1/auth/delete").route(web::post().to(auth_post_delete))) + .service( + web::resource("/api/v1/auth/passkey/register_start") + .route(web::post().to(auth_psk_create_start)), + ) + .service( + web::resource("/api/v1/auth/passkey/register_finish") + .route(web::post().to(auth_psk_create_finish)), + ) + .service( + web::resource("/api/v1/auth/passkey/auth_start/{username}") + .route(web::post().to(auth_psk_auth_start)), + ) + .service( + web::resource("/api/v1/auth/passkey/auth_finish") + .route(web::post().to(auth_psk_auth_finish)), + ) /* data endpoints */ - // GET (✅) - .service(web::resource("/api/v1/data").route(web::get().to(data_get_meta))) - .service(web::resource("/api/v1/data/detail/{id}").route(web::get().to(data_get_detailed))) - .service(web::resource("/api/v1/data/exists/{id}").route(web::get().to(data_get_exists))) - .service(web::resource("/api/v1/data/brief/team/{args}*").route(web::get().to(data_get_main_brief_team))) // season/event/team - .service(web::resource("/api/v1/data/brief/match/{args}*").route(web::get().to(data_get_main_brief_match))) // season/event/match_num - .service(web::resource("/api/v1/data/brief/event/{args}*").route(web::get().to(data_get_main_brief_event))) // season/event - .service(web::resource("/api/v1/data/brief/season/{args}*").route(web::get().to(data_get_main_brief_season))) // season/event - .service(web::resource("/api/v1/data/brief/user/{args}*").route(web::get().to(data_get_main_brief_user))) // season/user_id - .service(web::resource("/api/v1/data/teams/{args}*").route(web::get().to(data_get_main_teams))) // season/event - .service(web::resource("/api/v1/events/teams/{season}/{event}").route(web::get().to(event_get_frc_api))) - .service(web::resource("/api/v1/events/matches/{season}/{event}/{level}/{all}").route(web::get().to(event_get_frc_api_matches))) - // POST (✅) - .service(web::resource("/api/v1/data/submit").route(web::post().to(data_post_submit))) + // GET (✅) + .service(web::resource("/api/v1/data").route(web::get().to(data_get_meta))) + .service( + web::resource("/api/v1/data/detail/{id}").route(web::get().to(data_get_detailed)), + ) + .service( + web::resource("/api/v1/data/exists/{id}").route(web::get().to(data_get_exists)), + ) + .service( + web::resource("/api/v1/data/brief/team/{args}*") + .route(web::get().to(data_get_main_brief_team)), + ) // season/event/team + .service( + web::resource("/api/v1/data/brief/match/{args}*") + .route(web::get().to(data_get_main_brief_match)), + ) // season/event/match_num + .service( + web::resource("/api/v1/data/brief/event/{args}*") + .route(web::get().to(data_get_main_brief_event)), + ) // season/event + .service( + web::resource("/api/v1/data/brief/season/{args}*") + .route(web::get().to(data_get_main_brief_season)), + ) // season/event + .service( + web::resource("/api/v1/data/brief/user/{args}*") + .route(web::get().to(data_get_main_brief_user)), + ) // season/user_id + .service( + web::resource("/api/v1/data/teams/{args}*") + .route(web::get().to(data_get_main_teams)), + ) // season/event + .service( + web::resource("/api/v1/events/teams/{season}/{event}") + .route(web::get().to(event_get_frc_api)), + ) + .service( + web::resource("/api/v1/events/matches/{season}/{event}/{level}/{all}") + .route(web::get().to(event_get_frc_api_matches)), + ) + // POST (✅) + .service(web::resource("/api/v1/data/submit").route(web::post().to(data_post_submit))) /* manage endpoints */ - // GET - .service(web::resource("/api/v1/manage/submission_ids/{args}").route(web::get().to(manage_get_submission_ids))) - .service(web::resource("/api/v1/manage/all_users").route(web::get().to(manage_get_all_users))) - .service(web::resource("/api/v1/manage/team_users").route(web::get().to(manage_get_all_users_in_team))) - .service(web::resource("/api/v1/manage/all_access_keys").route(web::get().to(manage_get_all_keys))) - // DELETE - .service(web::resource("/api/v1/manage/delete/{id}").route(web::delete().to(manage_delete_submission))) - .service(web::resource("/api/v1/manage/user/delete/{user_id}").route(web::delete().to(manage_delete_user))) - .service(web::resource("/api/v1/manage/user/team_admin_delete/{user_id}").route(web::delete().to(manage_delete_user_team_admin))) - .service(web::resource("/api/v1/manage/access_key/delete/{access_key_id}").route(web::delete().to(manage_delete_access_key))) - // PATCH - .service(web::resource("/api/v1/manage/user/update_admin/{user_id}/{admin}").route(web::patch().to(manage_patch_admin))) - .service(web::resource("/api/v1/manage/user/update_team_admin/{user_id}/{admin}").route(web::patch().to(manage_patch_team_admin))) - .service(web::resource("/api/v1/manage/user/update_points/{user_id}/{modify}").route(web::patch().to(manage_patch_points))) - .service(web::resource("/api/v1/manage/access_key/update/{id}/{key}").route(web::patch().to(manage_patch_access_key))) - // POST - .service(web::resource("/api/v1/manage/access_key/create/{key}/{team}").route(web::post().to(manage_post_access_key))) + // GET + .service( + web::resource("/api/v1/manage/submission_ids/{args}") + .route(web::get().to(manage_get_submission_ids)), + ) + .service( + web::resource("/api/v1/manage/all_users") + .route(web::get().to(manage_get_all_users)), + ) + .service( + web::resource("/api/v1/manage/team_users") + .route(web::get().to(manage_get_all_users_in_team)), + ) + .service( + web::resource("/api/v1/manage/all_access_keys") + .route(web::get().to(manage_get_all_keys)), + ) + .service( + web::resource("/api/v1/manage/data_dump/{args}*") + .route(web::get().to(manage_data_dump)), + ) + // DELETE + .service( + web::resource("/api/v1/manage/delete/{id}") + .route(web::delete().to(manage_delete_submission)), + ) + .service( + web::resource("/api/v1/manage/user/delete/{user_id}") + .route(web::delete().to(manage_delete_user)), + ) + .service( + web::resource("/api/v1/manage/user/team_admin_delete/{user_id}") + .route(web::delete().to(manage_delete_user_team_admin)), + ) + .service( + web::resource("/api/v1/manage/access_key/delete/{access_key_id}") + .route(web::delete().to(manage_delete_access_key)), + ) + // PATCH + .service( + web::resource("/api/v1/manage/user/update_admin/{user_id}/{admin}") + .route(web::patch().to(manage_patch_admin)), + ) + .service( + web::resource("/api/v1/manage/user/update_team_admin/{user_id}/{admin}") + .route(web::patch().to(manage_patch_team_admin)), + ) + .service( + web::resource("/api/v1/manage/user/update_points/{user_id}/{modify}") + .route(web::patch().to(manage_patch_points)), + ) + .service( + web::resource("/api/v1/manage/access_key/update/{id}/{key}") + .route(web::patch().to(manage_patch_access_key)), + ) + // POST + .service( + web::resource("/api/v1/manage/access_key/create/{key}/{team}") + .route(web::post().to(manage_post_access_key)), + ) /* user endpoints */ /* casino endpoints */ - // GET - .service(web::resource("/api/v1/casino/spin_thing").route(web::get().to(casino_wheel))) - .service(web::resource("/api/v1/casino/blackjack").route(web::get().to(casino::websocket_route))) + // GET + .service(web::resource("/api/v1/casino/spin_thing").route(web::get().to(casino_wheel))) + .service( + web::resource("/api/v1/casino/blackjack") + .route(web::get().to(casino::websocket_route)), + ) /* points endpoints */ - // GET - .service(web::resource("/api/v1/points/all").route(web::get().to(points_get_all))) + // GET + .service(web::resource("/api/v1/points/all").route(web::get().to(points_get_all))) /* misc endpoints */ - // GET - .service(web::resource("/api/v1/transact/me").route(web::get().to(misc_get_transact_me))) - .service(web::resource("/api/v1/ping").route(web::get().to(misc_ping))) - .service(web::resource("/api/v1/whoami").route(web::get().to(misc_get_whoami))) - .service(web::resource("/apple-app-site-association").route(web::get().to(misc_apple_app_site_association))) + // GET + .service( + web::resource("/api/v1/transact/me").route(web::get().to(misc_get_transact_me)), + ) + .service(web::resource("/api/v1/ping").route(web::get().to(misc_ping))) + .service(web::resource("/api/v1/whoami").route(web::get().to(misc_get_whoami))) + .service( + web::resource("/apple-app-site-association") + .route(web::get().to(misc_apple_app_site_association)), + ) /* debug endpoints */ - // GET - .service(web::resource("/api/v1/debug/user").route(web::get().to(debug_get_user))) - .service(web::resource("/api/v1/debug/system").route(web::get().to(debug_health))) - .service(web::resource("/api/v1/debug/ok").route(web::get().to(debug_ok))) + // GET + .service(web::resource("/api/v1/debug/user").route(web::get().to(debug_get_user))) + .service(web::resource("/api/v1/debug/system").route(web::get().to(debug_health))) + .service(web::resource("/api/v1/debug/ok").route(web::get().to(debug_ok))) /* robot game endpoints */ - // GET - .service(web::resource("/api/v1/game/all_owned_cards").route(web::get().to(game_get_cards))) - .service(web::resource("/api/v1/game/team_data/{season}/{event}/{team}").route(web::get().to(game_get_team))) - .service(web::resource("/api/v1/game/open_lootbox").route(web::get().to(game_open_lootbox))) - // POST - .service(web::resource("/api/v1/game/set_hand").route(web::post().to(game_set_hand))) + // GET + .service( + web::resource("/api/v1/game/all_owned_cards").route(web::get().to(game_get_cards)), + ) + .service( + web::resource("/api/v1/game/team_data/{season}/{event}/{team}") + .route(web::get().to(game_get_team)), + ) + .service( + web::resource("/api/v1/game/open_lootbox").route(web::get().to(game_open_lootbox)), + ) + // POST + .service(web::resource("/api/v1/game/set_hand").route(web::post().to(game_set_hand))) }) - .bind_openssl(format!("{}:443", env::var("HOSTNAME").unwrap_or_else(|_| "localhost".to_string())), builder)? - .bind((env::var("HOSTNAME").unwrap_or_else(|_| "localhost".to_string()), 80))? + .bind_openssl( + format!( + "{}:443", + env::var("HOSTNAME").unwrap_or_else(|_| "localhost".to_string()) + ), + builder, + )? + .bind(( + env::var("HOSTNAME").unwrap_or_else(|_| "localhost".to_string()), + 80, + ))? .workers(8) .run() .await diff --git a/src/passkey.rs b/src/passkey.rs index 24f96f7e..da6ac1ca 100644 --- a/src/passkey.rs +++ b/src/passkey.rs @@ -1,8 +1,8 @@ -use std::{env, fmt, sync::RwLock}; use actix_identity::Identity; -use actix_session::{SessionGetError, SessionInsertError, Session}; +use actix_session::{Session, SessionGetError, SessionInsertError}; use actix_web::{web, HttpResponse}; use log::{error, info}; +use std::{env, fmt, sync::RwLock}; use webauthn_rs::prelude::*; use crate::db_auth; @@ -12,7 +12,11 @@ pub fn setup_passkeys() -> web::Data { let rp_origin = Url::parse(format!("https://{}", &rp_id).as_str()).expect("[PASSKEY] bad url"); let builder = WebauthnBuilder::new(&rp_id.as_str(), &rp_origin).expect("[PASSKEY] bad config"); let builder = builder.rp_name("bearTracks"); - let webauthn = web::Data::new(builder.build().expect("[PASSKEY] bad config (at build step)")); + let webauthn = web::Data::new( + builder + .build() + .expect("[PASSKEY] bad config (at build step)"), + ); webauthn } @@ -61,7 +65,12 @@ impl actix_web::ResponseError for Error { } } -pub async fn webauthn_start_registration(auth_pool: &db_auth::Pool, user: db_auth::User, session: Session, webauthn: web::Data) -> WebauthnResult> { +pub async fn webauthn_start_registration( + auth_pool: &db_auth::Pool, + user: db_auth::User, + session: Session, + webauthn: web::Data, +) -> WebauthnResult> { session.remove("reg_state"); let existing_keys = db_auth::get_passkeys(auth_pool, user.id.to_string()).await; if existing_keys.is_err() { @@ -69,16 +78,22 @@ pub async fn webauthn_start_registration(auth_pool: &db_auth::Pool, user: db_aut } let existing_keys = existing_keys.unwrap(); - let existing_keys = existing_keys.iter().map(|key| { - Some(key.cred_id().clone()) - }).collect::>>(); + let existing_keys = existing_keys + .iter() + .map(|key| Some(key.cred_id().clone())) + .collect::>>(); let (ccr, reg_state) = webauthn - .start_passkey_registration(Uuid::from_u128(user.id as u128), &user.username, &user.username, existing_keys) - .map_err(|err| { - info!("challenge_register_start → {:?}", err); - Error::Unknown(err) - })?; + .start_passkey_registration( + Uuid::from_u128(user.id as u128), + &user.username, + &user.username, + existing_keys, + ) + .map_err(|err| { + info!("challenge_register_start → {:?}", err); + Error::Unknown(err) + })?; if let Err(err) = session.insert("reg_state", ®_state) { error!("Failed to save reg_state to session storage!"); @@ -89,7 +104,13 @@ pub async fn webauthn_start_registration(auth_pool: &db_auth::Pool, user: db_aut Ok(web::Json(ccr)) } -pub async fn webauthn_finish_registration(auth_pool: &db_auth::Pool, user: db_auth::User, data: web::Json, session: Session, webauthn: web::Data) -> WebauthnResult { +pub async fn webauthn_finish_registration( + auth_pool: &db_auth::Pool, + user: db_auth::User, + data: web::Json, + session: Session, + webauthn: web::Data, +) -> WebauthnResult { let reg_state = match session.get("reg_state")? { Some(reg_state) => reg_state, None => return Err(Error::CorruptSession), @@ -98,11 +119,11 @@ pub async fn webauthn_finish_registration(auth_pool: &db_auth::Pool, user: db_au session.remove("reg_state"); let new_passkey = webauthn - .finish_passkey_registration(&data, ®_state) - .map_err(|e| { - info!("challenge_register_finish → {:?}", e); - Error::BadRequest(e) - })?; + .finish_passkey_registration(&data, ®_state) + .map_err(|e| { + info!("challenge_register_finish → {:?}", e); + Error::BadRequest(e) + })?; let insert = db_auth::set_passkey(auth_pool, new_passkey, user.id).await; if insert.is_err() { @@ -113,14 +134,20 @@ pub async fn webauthn_finish_registration(auth_pool: &db_auth::Pool, user: db_au Ok(HttpResponse::Ok().finish()) } -pub async fn webauthn_start_authentication(auth_pool: &db_auth::Pool, username: String, session: Session, webauthn: web::Data) -> WebauthnResult> { +pub async fn webauthn_start_authentication( + auth_pool: &db_auth::Pool, + username: String, + session: Session, + webauthn: web::Data, +) -> WebauthnResult> { session.remove("auth_state"); let target_user = db_auth::get_user_username(auth_pool, username.clone()).await; if target_user.is_err() { return Err(Error::UserNotFound); } - let user_credentials = db_auth::get_passkeys(auth_pool, target_user.unwrap().id.to_string()).await; + let user_credentials = + db_auth::get_passkeys(auth_pool, target_user.unwrap().id.to_string()).await; if user_credentials.is_err() { return Err(Error::UserHasNoCredentials); } @@ -130,11 +157,11 @@ pub async fn webauthn_start_authentication(auth_pool: &db_auth::Pool, username: } let (rcr, auth_state) = webauthn - .start_passkey_authentication(&user_credentials) - .map_err(|e| { - info!("challenge_authenticate_start → {:?}", e); - Error::Unknown(e) - })?; + .start_passkey_authentication(&user_credentials) + .map_err(|e| { + info!("challenge_authenticate_start → {:?}", e); + Error::Unknown(e) + })?; session.insert("auth_state", (username, auth_state))?; @@ -142,26 +169,37 @@ pub async fn webauthn_start_authentication(auth_pool: &db_auth::Pool, username: Ok(web::Json(rcr)) } -pub async fn webauthn_finish_authentication(auth_pool: &db_auth::Pool, cred: web::Json, session: Session, identity: Identity, webauthn: web::Data, sessions: web::Data>) -> WebauthnResult { - let (username, auth_state): (String, PasskeyAuthentication) = session.get("auth_state")?.ok_or(Error::CorruptSession)?; +pub async fn webauthn_finish_authentication( + auth_pool: &db_auth::Pool, + cred: web::Json, + session: Session, + identity: Identity, + webauthn: web::Data, + sessions: web::Data>, +) -> WebauthnResult { + let (username, auth_state): (String, PasskeyAuthentication) = + session.get("auth_state")?.ok_or(Error::CorruptSession)?; session.remove("auth_state"); let _auth_result = webauthn - .finish_passkey_authentication(&cred, &auth_state) - .map_err(|e| { - info!("challenge_authenticate_finish → {:?}", e); - Error::BadRequest(e) - })?; - + .finish_passkey_authentication(&cred, &auth_state) + .map_err(|e| { + info!("challenge_authenticate_finish → {:?}", e); + Error::BadRequest(e) + })?; + identity.remember(username.clone()); let target_user_temp = db_auth::get_user_username(&auth_pool, username.clone()).await; if target_user_temp.is_err() { - return Err(Error::BadRequest(WebauthnError::UserNotPresent)); + return Err(Error::BadRequest(WebauthnError::UserNotPresent)); } let target_user = target_user_temp.unwrap(); - sessions.write().unwrap().user_map.insert(target_user.clone().username.to_string(), target_user.clone()); + sessions.write().unwrap().user_map.insert( + target_user.clone().username.to_string(), + target_user.clone(), + ); info!("[PASSKEY] auth success"); Ok(HttpResponse::Ok().finish()) diff --git a/src/server_health.rs b/src/server_health.rs index 85a2d08b..6192e5a5 100644 --- a/src/server_health.rs +++ b/src/server_health.rs @@ -1,6 +1,6 @@ -use std::{env, sync::RwLock}; use actix_web::web; use serde::Serialize; +use std::{env, sync::RwLock}; use sysinfo::SystemExt; use crate::Sessions; @@ -36,6 +36,6 @@ pub fn get_server_health(session: web::Data>) -> HealthData { load_avg_one: system.get_load_average().one, load_avg_five: system.get_load_average().five, load_avg_fifteen: system.get_load_average().fifteen, - sessions_size: session.read().unwrap().user_map.len() as i64 + sessions_size: session.read().unwrap().user_map.len() as i64, } } diff --git a/src/session.rs b/src/session.rs index 83b83b5d..2b68818c 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,4 +1,3 @@ -use std::{collections::HashMap, ops::Add, sync::Mutex}; use actix_session::storage::{LoadError, SaveError, SessionKey, SessionStore, UpdateError}; use actix_web::cookie::time::Duration; use anyhow::anyhow; @@ -6,8 +5,10 @@ use async_trait::async_trait; use chrono::Utc; use once_cell::sync::Lazy; use rand::distributions::{Alphanumeric, DistString}; +use std::{collections::HashMap, ops::Add, sync::Mutex}; -static SESSION_STATES: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); +static SESSION_STATES: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); pub(crate) struct State { session_state: HashMap, @@ -19,7 +20,10 @@ pub(crate) struct MemorySession; #[async_trait(?Send)] impl SessionStore for MemorySession { - async fn load(&self, session_key: &SessionKey) -> Result>, LoadError> { + async fn load( + &self, + session_key: &SessionKey, + ) -> Result>, LoadError> { let now = Utc::now(); Ok(SESSION_STATES @@ -65,7 +69,12 @@ impl SessionStore for MemorySession { .map_err(|_| SaveError::Serialization(anyhow!("invalid session key")))?) } - async fn update(&self, session_key: SessionKey, session_state: HashMap, ttl: &Duration) -> Result { + async fn update( + &self, + session_key: SessionKey, + session_state: HashMap, + ttl: &Duration, + ) -> Result { if let Some(entry) = SESSION_STATES .lock() .map_err(|_| UpdateError::Other(anyhow!("poison error")))? @@ -77,13 +86,15 @@ impl SessionStore for MemorySession { Ok(session_key) } else { - Err(UpdateError::Other(anyhow!( - "invalid session" - ))) + Err(UpdateError::Other(anyhow!("invalid session"))) } } - async fn update_ttl(&self, session_key: &SessionKey, ttl: &Duration) -> Result<(), anyhow::Error> { + async fn update_ttl( + &self, + session_key: &SessionKey, + ttl: &Duration, + ) -> Result<(), anyhow::Error> { if let Some(entry) = SESSION_STATES .lock() .map_err(|_| anyhow!("poison error"))? @@ -104,4 +115,4 @@ impl SessionStore for MemorySession { Ok(()) } -} \ No newline at end of file +} diff --git a/src/static_files.rs b/src/static_files.rs index ff1631a6..816ac101 100644 --- a/src/static_files.rs +++ b/src/static_files.rs @@ -1,5 +1,8 @@ -use std::{env, include_str, include_bytes}; -use actix_web::{HttpRequest, HttpResponse, http::header::{ContentType, CacheControl, CacheDirective}}; +use actix_web::{ + http::header::{CacheControl, CacheDirective, ContentType}, + HttpRequest, HttpResponse, +}; +use std::{env, include_bytes, include_str}; // bundle static files in binary const INDEX_HTML: &str = include_str!("../static/index.html"); @@ -26,19 +29,22 @@ const FAVICON_ICO: &[u8] = include_bytes!("../static/favicon.ico"); pub async fn static_index(req: HttpRequest) -> HttpResponse { // redirect requests not to port 443 (port 80) to 443 match req.app_config().local_addr().port() { - 443 => { - HttpResponse::Ok() - .content_type(ContentType::html()) - .insert_header(CacheControl(vec![ - CacheDirective::Public, - CacheDirective::MaxAge(23328000u32), - ])) - .body(INDEX_HTML) - } + 443 => HttpResponse::Ok() + .content_type(ContentType::html()) + .insert_header(CacheControl(vec![ + CacheDirective::Public, + CacheDirective::MaxAge(23328000u32), + ])) + .body(INDEX_HTML), _ => HttpResponse::PermanentRedirect() - .append_header( - ("location", format!("https://{}", env::var("HOSTNAME").unwrap_or_else(|_| "localhost".to_string()))) - ).finish(), + .append_header(( + "location", + format!( + "https://{}", + env::var("HOSTNAME").unwrap_or_else(|_| "localhost".to_string()) + ), + )) + .finish(), } } @@ -119,41 +125,59 @@ pub async fn static_spin() -> HttpResponse { pub async fn static_android_chrome_192() -> HttpResponse { HttpResponse::Ok() .append_header(("Content-Type", "image/png")) - .insert_header(CacheControl(vec![CacheDirective::Public, CacheDirective::MaxAge(4838400u32)])) + .insert_header(CacheControl(vec![ + CacheDirective::Public, + CacheDirective::MaxAge(4838400u32), + ])) .body(ANDROID_CHROME_192) } pub async fn static_android_chrome_512() -> HttpResponse { HttpResponse::Ok() .append_header(("Content-Type", "image/png")) - .insert_header(CacheControl(vec![CacheDirective::Public, CacheDirective::MaxAge(4838400u32)])) + .insert_header(CacheControl(vec![ + CacheDirective::Public, + CacheDirective::MaxAge(4838400u32), + ])) .body(ANDROID_CHROME_512) } pub async fn static_apple_touch_icon() -> HttpResponse { HttpResponse::Ok() .append_header(("Content-Type", "image/png")) - .insert_header(CacheControl(vec![CacheDirective::Public, CacheDirective::MaxAge(4838400u32)])) + .insert_header(CacheControl(vec![ + CacheDirective::Public, + CacheDirective::MaxAge(4838400u32), + ])) .body(APPLE_TOUCH_ICON) } pub async fn static_favicon_16() -> HttpResponse { HttpResponse::Ok() .append_header(("Content-Type", "image/png")) - .insert_header(CacheControl(vec![CacheDirective::Public, CacheDirective::MaxAge(4838400u32)])) + .insert_header(CacheControl(vec![ + CacheDirective::Public, + CacheDirective::MaxAge(4838400u32), + ])) .body(FAVICON_16) } pub async fn static_favicon_32() -> HttpResponse { HttpResponse::Ok() .append_header(("Content-Type", "image/png")) - .insert_header(CacheControl(vec![CacheDirective::Public, CacheDirective::MaxAge(4838400u32)])) + .insert_header(CacheControl(vec![ + CacheDirective::Public, + CacheDirective::MaxAge(4838400u32), + ])) .body(FAVICON_32) } pub async fn static_favicon() -> HttpResponse { HttpResponse::Ok() .append_header(("Content-Type", "image/x-icon")) - .insert_header(CacheControl(vec![CacheDirective::Public, CacheDirective::MaxAge(4838400u32)])) + .insert_header(CacheControl(vec![ + CacheDirective::Public, + CacheDirective::MaxAge(4838400u32), + ])) .body(FAVICON_ICO) -} \ No newline at end of file +} diff --git a/src/stats.rs b/src/stats.rs index 6f07b69c..af4f7621 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -6,33 +6,48 @@ pub fn quartiles_i64(data: &Vec) -> Vec { let mut quartiles: Vec = Vec::new(); // first quartile if (data_length * 0.25) % 1.0 == 0.0 { - quartiles.push((sorted_data[((data_length * 0.25) - 1.0) as usize] + sorted_data[(data_length * 0.25) as usize]) / 2 as i64); + quartiles.push( + (sorted_data[((data_length * 0.25) - 1.0) as usize] + + sorted_data[(data_length * 0.25) as usize]) + / 2 as i64, + ); } else { quartiles.push(sorted_data[(data_length * 0.25).floor() as usize]); } // second quartile (median) if (data_length * 0.5) % 1.0 == 0.0 { - quartiles.push((sorted_data[((data_length * 0.5) - 1.0) as usize] + sorted_data[(data_length * 0.5) as usize]) / 2 as i64); + quartiles.push( + (sorted_data[((data_length * 0.5) - 1.0) as usize] + + sorted_data[(data_length * 0.5) as usize]) + / 2 as i64, + ); } else { quartiles.push(sorted_data[(data_length * 0.5).floor() as usize]); } // third quartile if (data_length * 0.75) % 1.0 == 0.0 { - quartiles.push((sorted_data[((data_length * 0.75) - 1.0) as usize] + sorted_data[(data_length * 0.75) as usize]) / 2 as i64); + quartiles.push( + (sorted_data[((data_length * 0.75) - 1.0) as usize] + + sorted_data[(data_length * 0.75) as usize]) + / 2 as i64, + ); } else { quartiles.push(sorted_data[(data_length * 0.75).floor() as usize]); } - return quartiles + return quartiles; } - return vec!(0, 0, 0) + return vec![0, 0, 0]; } pub fn means_i64(data: &Vec, first_wt: f64) -> Vec { if data.len() > 0 { let mut means: Vec = Vec::new(); means.push(data.iter().sum::() / data.len() as i64); - means.push(((data[0] as f64 * first_wt) + (data.iter().sum::() as f64 * (1.0 - first_wt))) as i64); - return means + means.push( + ((data[0] as f64 * first_wt) + (data.iter().sum::() as f64 * (1.0 - first_wt))) + as i64, + ); + return means; } - return vec!(0, 0); -} \ No newline at end of file + return vec![0, 0]; +}