diff --git a/.gitignore b/.gitignore index 77528937246d710da3c140a20ed19ce6c610bae1..0f25e70632f5e5cc8592e87c9ab7a9d3004ba889 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ OSX/dsa_priv.pem bin/config.json bin/release/aws-eb/metabase-aws-eb.zip *.sqlite +/reset-password-artifacts diff --git a/OSX/Metabase.xcodeproj/project.pbxproj b/OSX/Metabase.xcodeproj/project.pbxproj index a8d8b8e7704bd269c0071874e4b1cac88d2bd1a6..7c39d5f41b6d2f87bbca706a5546ebf1a817ea97 100644 --- a/OSX/Metabase.xcodeproj/project.pbxproj +++ b/OSX/Metabase.xcodeproj/project.pbxproj @@ -41,6 +41,9 @@ D18855611BB1C8D700D89803 /* jre in Resources */ = {isa = PBXBuildFile; fileRef = D188555E1BB1C86F00D89803 /* jre */; }; D1BDAC681C053E0A0075D3AC /* ResetPasswordWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = D1BDAC671C053E0A0075D3AC /* ResetPasswordWindowController.m */; }; D1BDAC6A1C053E220075D3AC /* ResetPasswordWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D1BDAC691C053E220075D3AC /* ResetPasswordWindowController.xib */; }; + D1BDAC6D1C0565640075D3AC /* JavaTask.m in Sources */ = {isa = PBXBuildFile; fileRef = D1BDAC6C1C0565640075D3AC /* JavaTask.m */; }; + D1BDAC721C05695B0075D3AC /* ResetPasswordTask.m in Sources */ = {isa = PBXBuildFile; fileRef = D1BDAC711C05695B0075D3AC /* ResetPasswordTask.m */; }; + D1BDAC731C056C7C0075D3AC /* reset-password.jar in Resources */ = {isa = PBXBuildFile; fileRef = D1BDAC6E1C0566490075D3AC /* reset-password.jar */; }; D1CB9FE81BCEDE9A009A61FB /* LoadingView.m in Sources */ = {isa = PBXBuildFile; fileRef = D1CB9FE71BCEDE9A009A61FB /* LoadingView.m */; }; D1CB9FEA1BCEDEA5009A61FB /* LoadingView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D1CB9FE91BCEDEA5009A61FB /* LoadingView.xib */; }; D1CB9FED1BCEDF02009A61FB /* Loading_View_Inner.png in Resources */ = {isa = PBXBuildFile; fileRef = D1CB9FEB1BCEDF02009A61FB /* Loading_View_Inner.png */; }; @@ -124,6 +127,11 @@ D1BDAC661C053E0A0075D3AC /* ResetPasswordWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResetPasswordWindowController.h; sourceTree = "<group>"; }; D1BDAC671C053E0A0075D3AC /* ResetPasswordWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ResetPasswordWindowController.m; sourceTree = "<group>"; }; D1BDAC691C053E220075D3AC /* ResetPasswordWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ResetPasswordWindowController.xib; sourceTree = "<group>"; }; + D1BDAC6B1C0565640075D3AC /* JavaTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JavaTask.h; sourceTree = "<group>"; }; + D1BDAC6C1C0565640075D3AC /* JavaTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JavaTask.m; sourceTree = "<group>"; }; + D1BDAC6E1C0566490075D3AC /* reset-password.jar */ = {isa = PBXFileReference; lastKnownFileType = archive.jar; path = "reset-password.jar"; sourceTree = "<group>"; }; + D1BDAC701C05695B0075D3AC /* ResetPasswordTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResetPasswordTask.h; sourceTree = "<group>"; }; + D1BDAC711C05695B0075D3AC /* ResetPasswordTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ResetPasswordTask.m; sourceTree = "<group>"; }; D1CB9FE61BCEDE9A009A61FB /* LoadingView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LoadingView.h; sourceTree = "<group>"; }; D1CB9FE71BCEDE9A009A61FB /* LoadingView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LoadingView.m; sourceTree = "<group>"; }; D1CB9FE91BCEDEA5009A61FB /* LoadingView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LoadingView.xib; sourceTree = "<group>"; }; @@ -231,12 +239,16 @@ children = ( D162C4991BC87D2B009F678F /* AppDelegate.h */, D162C49A1BC87D2B009F678F /* AppDelegate.m */, - D162C49B1BC87D2B009F678F /* MetabaseTask.h */, - D162C49C1BC87D2B009F678F /* MetabaseTask.m */, D162C49D1BC87D2B009F678F /* SettingsManager.h */, D162C49E1BC87D2B009F678F /* SettingsManager.m */, D162C49F1BC87D2B009F678F /* TaskHealthChecker.h */, D162C4A01BC87D2B009F678F /* TaskHealthChecker.m */, + D1BDAC6B1C0565640075D3AC /* JavaTask.h */, + D1BDAC6C1C0565640075D3AC /* JavaTask.m */, + D162C49B1BC87D2B009F678F /* MetabaseTask.h */, + D162C49C1BC87D2B009F678F /* MetabaseTask.m */, + D1BDAC701C05695B0075D3AC /* ResetPasswordTask.h */, + D1BDAC711C05695B0075D3AC /* ResetPasswordTask.m */, ); path = Backend; sourceTree = "<group>"; @@ -326,6 +338,7 @@ children = ( D18853D61BB0CEC600D89803 /* Images.xcassets */, D105B2321BB5BE4A00A5D850 /* Images */, + D1BDAC6E1C0566490075D3AC /* reset-password.jar */, D18854021BB0DB6000D89803 /* metabase.jar */, D121FD681BC5B4E7002101B0 /* dsa_pub.pem */, ); @@ -413,6 +426,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + D1BDAC731C056C7C0075D3AC /* reset-password.jar in Resources */, D1DFF6C31BCEF9E700ECC7B6 /* Loading_View_Inner@2x.png in Resources */, D1CB9FEE1BCEDF02009A61FB /* Loading_View_Outer.png in Resources */, D105B23D1BB5BE4A00A5D850 /* back_icon@2x.png in Resources */, @@ -478,6 +492,8 @@ D162C4AF1BC87D2B009F678F /* MainViewController.m in Sources */, D105B2281BB378C100A5D850 /* INWindowButton.m in Sources */, D105B2271BB378C100A5D850 /* INAppStoreWindow.m in Sources */, + D1BDAC6D1C0565640075D3AC /* JavaTask.m in Sources */, + D1BDAC721C05695B0075D3AC /* ResetPasswordTask.m in Sources */, D162C4A91BC87D2B009F678F /* MetabaseTask.m in Sources */, D162C4A81BC87D2B009F678F /* AppDelegate.m in Sources */, D1BDAC681C053E0A0075D3AC /* ResetPasswordWindowController.m in Sources */, diff --git a/OSX/Metabase/Backend/JavaTask.h b/OSX/Metabase/Backend/JavaTask.h new file mode 100644 index 0000000000000000000000000000000000000000..c890ecc031b042629f43eac093058fa123163332 --- /dev/null +++ b/OSX/Metabase/Backend/JavaTask.h @@ -0,0 +1,25 @@ +// +// JavaTask.h +// Metabase +// +// Created by Cam Saul on 11/24/15. +// Copyright (c) 2015 Metabase. All rights reserved. +// + +NSString *JREPath(); +NSString *UberjarPath(); +NSString *DBPath(); ///< Path to the H2 DB file + +/// Base class for running JRE-based NSTasks +@interface JavaTask : NSObject + +@property (strong, nonatomic) NSTask *task; + +/// Called when a new data is written to the task's stdin / stdout. Default implementation does nothing. This is called on a background thread! +- (void)readHandleDidRead:(NSString *)message; + +/// Terminate the associated NSTask. +- (void)terminate; + + +@end diff --git a/OSX/Metabase/Backend/JavaTask.m b/OSX/Metabase/Backend/JavaTask.m new file mode 100644 index 0000000000000000000000000000000000000000..3ca36118ea998af63f75aa40be68e6a099dacb7f --- /dev/null +++ b/OSX/Metabase/Backend/JavaTask.m @@ -0,0 +1,137 @@ +// +// JavaTask.m +// Metabase +// +// Created by Cam Saul on 11/24/15. +// Copyright (c) 2015 Metabase. All rights reserved. +// + +#import "JavaTask.h" + +NSString *JREPath() { + return [[NSBundle mainBundle] pathForResource:@"java" ofType:nil inDirectory:@"jre/bin"]; +} + +NSString *UberjarPath() { + return [[NSBundle mainBundle] pathForResource:@"metabase" ofType:@"jar"]; +} + +NSString *DBPath() { + NSString *applicationSupportDir = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"Metabase"]; + if (![[NSFileManager defaultManager] fileExistsAtPath:applicationSupportDir]) { + NSError *error = nil; + [[NSFileManager defaultManager] createDirectoryAtPath:applicationSupportDir withIntermediateDirectories:YES attributes:nil error:&error]; + if (error) { + NSLog(@"Error creating %@: %@", applicationSupportDir, error.localizedDescription); + } + } + return [applicationSupportDir stringByAppendingPathComponent:@"metabase.db"]; +} + + +@interface JavaTask () +@property (strong, nonatomic, readwrite) NSPipe *pipe; +@property (strong, nonatomic, readwrite) NSFileHandle *readHandle; +@end + +@implementation JavaTask + +#pragma mark - Lifecycle + +- (instancetype)init { + if (self = [super init]) { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fileHandleCompletedRead:) name:NSFileHandleReadCompletionNotification object:nil]; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [self terminate]; +} + + +#pragma mark - Notifications + +- (void)fileHandleCompletedRead:(NSNotification *)notification { + if (!self.readHandle || notification.object != self.readHandle) return; + + __weak JavaTask *weakSelf = self; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + if (!weakSelf) return; + + NSData *data = notification.userInfo[NSFileHandleNotificationDataItem]; + if (data.length) [weakSelf readHandleDidReadData:data]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf.readHandle readInBackgroundAndNotify]; + }); + }); +} + + + +#pragma mark - Local Methods + +- (void)readHandleDidReadData:(NSData *)data { + NSString *message = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if (!message.length) return; + + [self readHandleDidRead:message]; +} + +- (void)readHandleDidRead:(NSString *)message {} + +- (void)terminate { + if (!self.task) return; // already dead + + NSLog(@"Killing %@ @ 0x%zx...", [self class], (size_t)self); + self.task = nil; +} + + +#pragma mark - Getters / Setters + +- (void)setTask:(NSTask *)task { + self.pipe = nil; + + [_task terminate]; + _task = task; + + if (task) { + self.pipe = [NSPipe pipe]; + self.task.standardOutput = self.pipe; + self.task.standardError = self.pipe; + } +} + +- (void)setPipe:(NSPipe *)pipe { + self.readHandle = nil; + _pipe = pipe; + + if (pipe) { + self.readHandle = pipe.fileHandleForReading; + __weak JavaTask *weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + if (weakSelf) [weakSelf.readHandle readInBackgroundAndNotify]; + }); + } +} + +- (void)setReadHandle:(NSFileHandle *)readHandle { + // handle any remaining data in the read handle before closing, if applicable + if (_readHandle) { + NSData *data; + @try { + data = [_readHandle availableData]; + } + @catch (NSException *exception) {} + + if (data.length) [self readHandleDidReadData:data]; + } + + [_readHandle closeFile]; + _readHandle = readHandle; +} + +@end diff --git a/OSX/Metabase/Backend/MetabaseTask.h b/OSX/Metabase/Backend/MetabaseTask.h index fd15ae30fe732858b6b6e66def8f09dfe286a56a..f71efc48b2b6d00e8e165cc6b5df887f351cfacc 100644 --- a/OSX/Metabase/Backend/MetabaseTask.h +++ b/OSX/Metabase/Backend/MetabaseTask.h @@ -6,13 +6,15 @@ // Copyright (c) 2015 Metabase. All rights reserved. // -@interface MetabaseTask : NSObject +#import "JavaTask.h" + +/// Task for running the MetabaseServer +@interface MetabaseTask : JavaTask /// Create (and launch) a task to run the Metabase backend server. + (MetabaseTask *)task; - (void)launch; -- (void)terminate; - (NSUInteger)port; diff --git a/OSX/Metabase/Backend/MetabaseTask.m b/OSX/Metabase/Backend/MetabaseTask.m index 7d79c0fff83e60be2e1557d5a8397d7bc33265ee..0668a16240a84abdd747338f6bd99d717dfdee09 100644 --- a/OSX/Metabase/Backend/MetabaseTask.m +++ b/OSX/Metabase/Backend/MetabaseTask.m @@ -11,13 +11,6 @@ #define ENABLE_JAR_UNPACKING 0 @interface MetabaseTask () -@property (strong, nonatomic) NSTask *task; -@property (strong, nonatomic) NSPipe *pipe; -@property (strong, nonatomic) NSFileHandle *readHandle; - -@property (strong, readonly) NSString *javaPath; -@property (strong, readonly) NSString *jarPath; -@property (strong, readonly) NSString *dbPath; #if ENABLE_JAR_UNPACKING @property (strong, readonly) NSString *unpack200Path; @@ -33,54 +26,23 @@ } -#pragma mark - Lifecycle - -- (instancetype)init { - if (self = [super init]) { - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(fileHandleCompletedRead:) name:NSFileHandleReadCompletionNotification object:nil]; - } - return self; -} - -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; - [self terminate]; -} - +#pragma mark - Local Methods -#pragma mark - Notifications +- (void)readHandleDidRead:(NSString *)message { + // skip calls to health endpoint + if ([message rangeOfString:@"GET /api/health"].location != NSNotFound) return; -- (void)fileHandleCompletedRead:(NSNotification *)notification { - if (!self.readHandle) return; + // strip off the timestamp that comes back from the backend so we don't get double-timestamps when NSLog adds its own + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^[\\d-:,\\s]+(.*$)" options:NSRegularExpressionAnchorsMatchLines|NSRegularExpressionAllowCommentsAndWhitespace error:nil]; + message = [regex stringByReplacingMatchesInString:message options:0 range:NSMakeRange(0, message.length) withTemplate:@"$1"]; - __weak MetabaseTask *weakSelf = self; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ - if (!weakSelf) return; - @try { - NSString *message = [[NSString alloc] initWithData:weakSelf.readHandle.availableData encoding:NSUTF8StringEncoding]; - // skip calls to health endpoint - if ([message rangeOfString:@"GET /api/health"].location == NSNotFound) { - // strip off the timestamp that comes back from the backend so we don't get double-timestamps when NSLog adds its own - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^[\\d-:,\\s]+(.*$)" options:NSRegularExpressionAnchorsMatchLines|NSRegularExpressionAllowCommentsAndWhitespace error:nil]; - message = [regex stringByReplacingMatchesInString:message options:0 range:NSMakeRange(0, message.length) withTemplate:@"$1"]; - - // remove control codes used to color output - regex = [NSRegularExpression regularExpressionWithPattern:@"\\[\\d+m" options:0 error:nil]; - message = [regex stringByReplacingMatchesInString:message options:0 range:NSMakeRange(0, message.length) withTemplate:@""]; - - NSLog(@"%@", message); - } - } @catch (NSException *) {} - - dispatch_async(dispatch_get_main_queue(), ^{ - [weakSelf.readHandle readInBackgroundAndNotify]; - }); - }); + // remove control codes used to color output + regex = [NSRegularExpression regularExpressionWithPattern:@"\\[\\d+m" options:0 error:nil]; + message = [regex stringByReplacingMatchesInString:message options:0 range:NSMakeRange(0, message.length) withTemplate:@""]; + + NSLog(@"%@", message); } - -#pragma mark - Local Methods - #if ENABLE_JAR_UNPACKING /// unpack the jars in the BG if needed the first time around - (void)unpackJars { @@ -100,8 +62,8 @@ #endif - (void)deleteOldDBLockFilesIfNeeded { - NSString *lockFile = [self.dbPath stringByAppendingString:@".lock.db"]; - NSString *traceFile = [self.dbPath stringByAppendingString:@".trace.db"]; + NSString *lockFile = [DBPath() stringByAppendingString:@".lock.db"]; + NSString *traceFile = [DBPath() stringByAppendingString:@".trace.db"]; for (NSString *file in @[lockFile, traceFile]) { if ([[NSFileManager defaultManager] fileExistsAtPath:file]) { @@ -129,15 +91,11 @@ NSLog(@"Starting MetabaseTask @ 0x%zx...", (size_t)self); self.task = [[NSTask alloc] init]; - self.task.launchPath = self.javaPath; - self.task.environment = @{@"MB_DB_FILE": self.dbPath, + self.task.launchPath = JREPath(); + self.task.environment = @{@"MB_DB_FILE": DBPath(), @"MB_JETTY_PORT": @(self.port)}; - self.task.arguments = @[@"-jar", self.jarPath]; - - self.pipe = [NSPipe pipe]; - self.task.standardOutput = self.pipe; - self.task.standardError = self.pipe; - + self.task.arguments = @[@"-jar", UberjarPath()]; + __weak MetabaseTask *weakSelf = self; self.task.terminationHandler = ^(NSTask *task){ NSLog(@"\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Task terminated with exit code %d !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", task.terminationStatus); @@ -149,64 +107,20 @@ } }); }; - - self.readHandle = self.pipe.fileHandleForReading; - dispatch_async(dispatch_get_main_queue(), ^{ - [self.readHandle readInBackgroundAndNotify]; - }); - - NSLog(@"%@ -jar %@", self.javaPath, self.jarPath); + + NSLog(@"Launching MetabaseTask\nMB_DB_FILE='%@' MB_JETTY_PORT=%lu %@ -jar %@", DBPath(), self.port, JREPath(), UberjarPath()); [self.task launch]; }); } - (void)terminate { - if (!self.task) return; // already dead - - NSLog(@"Killing MetabaseTask @ 0x%zx...", (size_t)self); - self.task = nil; + [super terminate]; _port = 0; } #pragma mark - Getters / Setters -- (void)setTask:(NSTask *)task { - self.pipe = nil; - [_task terminate]; - _task = task; -} - -- (void)setPipe:(NSPipe *)pipe { - self.readHandle = nil; - _pipe = pipe; -} - -- (void)setReadHandle:(NSFileHandle *)readHandle { - [_readHandle closeFile]; - _readHandle = readHandle; -} - -- (NSString *)javaPath { - return [[NSBundle mainBundle] pathForResource:@"java" ofType:nil inDirectory:@"jre/bin"]; -} - -- (NSString *)jarPath { - return [[NSBundle mainBundle] pathForResource:@"metabase" ofType:@"jar"]; -} - -- (NSString *)dbPath { - NSString *applicationSupportDir = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"Metabase"]; - if (![[NSFileManager defaultManager] fileExistsAtPath:applicationSupportDir]) { - NSError *error = nil; - [[NSFileManager defaultManager] createDirectoryAtPath:applicationSupportDir withIntermediateDirectories:YES attributes:nil error:&error]; - if (error) { - NSLog(@"Error creating %@: %@", applicationSupportDir, error.localizedDescription); - } - } - return [applicationSupportDir stringByAppendingPathComponent:@"metabase.db"]; -} - - (NSUInteger)port { if (!_port) { srand((unsigned)time(NULL)); diff --git a/OSX/Metabase/Backend/ResetPasswordTask.h b/OSX/Metabase/Backend/ResetPasswordTask.h new file mode 100644 index 0000000000000000000000000000000000000000..79027b1c00313542afa9781de4c32df1a74e8383 --- /dev/null +++ b/OSX/Metabase/Backend/ResetPasswordTask.h @@ -0,0 +1,16 @@ +// +// ResetPasswordTask.h +// Metabase +// +// Created by Cam Saul on 11/24/15. +// Copyright (c) 2015 Metabase. All rights reserved. +// + +#import "JavaTask.h" + +@interface ResetPasswordTask : JavaTask + +/// blocks are ran on main thread <3 +- (void)resetPasswordForEmailAddress:(NSString *)emailAddress success:(void(^)(NSString *resetToken))successBlock error:(void(^)(NSString *errorMessage))errorBlock; + +@end diff --git a/OSX/Metabase/Backend/ResetPasswordTask.m b/OSX/Metabase/Backend/ResetPasswordTask.m new file mode 100644 index 0000000000000000000000000000000000000000..07857a869c13ad230dcb4739516cead174005f83 --- /dev/null +++ b/OSX/Metabase/Backend/ResetPasswordTask.m @@ -0,0 +1,75 @@ +// +// ResetPasswordTask.m +// Metabase +// +// Created by Cam Saul on 11/24/15. +// Copyright (c) 2015 Metabase. All rights reserved. +// + +#import "ResetPasswordTask.h" + +@interface ResetPasswordTask () +@property (nonatomic, readonly) NSString *resetPasswordJarPath; +@property (copy) NSString *output; +@end + +@implementation ResetPasswordTask + + +#pragma mark - Local Methods + +- (void)resetPasswordForEmailAddress:(NSString *)emailAddress success:(void (^)(NSString *))successBlock error:(void (^)(NSString *))errorBlock { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + self.task = [[NSTask alloc] init]; + self.task.launchPath = JREPath(); + self.task.arguments = @[@"-classpath", [NSString stringWithFormat:@"%@:%@", UberjarPath(), self.resetPasswordJarPath], + @"metabase.reset_password.core", + DBPath(), emailAddress]; + + + __weak ResetPasswordTask *weakSelf = self; + self.task.terminationHandler = ^(NSTask *task) { + NSLog(@"ResetPasswordTask terminated with status: %d", task.terminationStatus); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5f * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [weakSelf terminate]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5f * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + if (!task.terminationStatus && weakSelf.output.length) { + successBlock(weakSelf.output); + } else { + errorBlock(weakSelf.output.length ? weakSelf.output : @"An unknown error has occured."); + } + }); + }); + }); + }; + + NSLog(@"Launching ResetPasswordTask\n%@ -classpath %@:%@ metabase.reset_password.core %@ %@", JREPath(), UberjarPath(), self.resetPasswordJarPath, DBPath(), emailAddress); + // delay lauch just a second to make sure pipe is all set up, etc. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5f * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self.task launch]; + }); + }); +} + +- (void)readHandleDidRead:(NSString *)message { + /// output comes back like "STATUS [[[message]]]" + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^(?:(?:OK)||(?:FAIL))\\s+\\[\\[\\[(.+)\\]\\]\\]\\s*$" options:NSRegularExpressionAnchorsMatchLines|NSRegularExpressionAllowCommentsAndWhitespace error:NULL]; + NSString *result = [regex stringByReplacingMatchesInString:message options:0 range:NSMakeRange(0, message.length) withTemplate:@"$1"]; + if (result) { + self.output = result; + NSLog(@"[PasswordResetTask] %@", self.output); + } else { + NSLog(@"[PasswordResetTask - Bad Output] %@", message); + } +} + + +#pragma mark - Getters / Setters + +- (NSString *)resetPasswordJarPath { + return [[NSBundle mainBundle] pathForResource:@"reset-password" ofType:@"jar"]; +} + +@end diff --git a/OSX/Metabase/Backend/TaskHealthChecker.h b/OSX/Metabase/Backend/TaskHealthChecker.h index 629375d13a45b39fddb90a412a6a35b64709329d..3d6f94e136b653415b24057ce6225dc6e29a2f35 100644 --- a/OSX/Metabase/Backend/TaskHealthChecker.h +++ b/OSX/Metabase/Backend/TaskHealthChecker.h @@ -10,6 +10,7 @@ static NSString * const MetabaseTaskBecameHealthyNotification = @"MetabaseTaskBe static NSString * const MetabaseTaskBecameUnhealthyNotification = @"MetabaseTaskBecameUnhealthyNotification"; static NSString * const MetabaseTaskTimedOutNotification = @"MetabaseTaskTimedOutNotification"; +/// Manages the MetabaseTask (server) and restarts it if it gets unresponsive @interface TaskHealthChecker : NSObject @property () NSUInteger port; diff --git a/OSX/Metabase/Backend/TaskHealthChecker.m b/OSX/Metabase/Backend/TaskHealthChecker.m index e1de91bded3d642987562dc3641e690dc31f09fb..c6a2f65c8445a076abec56c7eb30a4d6b398d6d5 100644 --- a/OSX/Metabase/Backend/TaskHealthChecker.m +++ b/OSX/Metabase/Backend/TaskHealthChecker.m @@ -15,7 +15,7 @@ static const CGFloat HealthCheckIntervalSeconds = 1.2f; static const CGFloat HealthCheckRequestTimeout = 0.25f; /// After this many seconds of being unhealthy, consider the task timed out so it can be killed -static const CFTimeInterval TimeoutIntervalSeconds = 10.0f; +static const CFTimeInterval TimeoutIntervalSeconds = 15.0f; @interface TaskHealthChecker () @property (strong, nonatomic) NSOperationQueue *healthCheckOperationQueue; @@ -54,10 +54,8 @@ static const CFTimeInterval TimeoutIntervalSeconds = 10.0f; self.healthCheckTimer = [NSTimer timerWithTimeInterval:HealthCheckIntervalSeconds target:self selector:@selector(checkHealth) userInfo:nil repeats:YES]; self.healthCheckTimer.tolerance = HealthCheckIntervalSeconds / 2.0f; [[NSRunLoop mainRunLoop] addTimer:self.healthCheckTimer forMode:NSRunLoopCommonModes]; - -// self.healthCheckTimer = [NSTimer scheduledTimerWithTimeInterval:HealthCheckIntervalSeconds target:self selector:@selector(checkHealth) userInfo:nil repeats:YES]; -// self.healthCheckTimer.tolerance = HealthCheckIntervalSeconds / 2.0f; // the timer doesn't need to fire exactly on the intervals, so give it a loose tolerance which will improve power savings, etc. - });} + }); +} - (void)stop { diff --git a/OSX/Metabase/UI/MainViewController.m b/OSX/Metabase/UI/MainViewController.m index 57908f4ae55c1e321f9a045169806f298774df97..a96e36e97cf35887d36f969bcfa9316bc578ad75 100644 --- a/OSX/Metabase/UI/MainViewController.m +++ b/OSX/Metabase/UI/MainViewController.m @@ -17,7 +17,14 @@ #import "SettingsManager.h" #import "TaskHealthChecker.h" -@interface MainViewController () + + +NSString *BaseURL() { + return SettingsManager.instance.baseURL.length ? SettingsManager.instance.baseURL : LocalHostBaseURL(); +} + + +@interface MainViewController () <ResetPasswordWindowControllerDelegate> @property (weak) IBOutlet WebView *webView; @property (strong) IBOutlet NSView *titleBarView; @@ -28,8 +35,6 @@ @property (weak) LoadingView *loadingView; -@property (strong, readonly) NSString *baseURL; - @property (nonatomic) BOOL loading; @end @@ -102,9 +107,9 @@ #pragma mark - Local Methods - (void)loadMainPage { - NSLog(@"Connecting to Metabase instance at: %@", self.baseURL); + NSLog(@"Connecting to Metabase instance at: %@", BaseURL()); - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.baseURL]]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:BaseURL()]]; request.cachePolicy = NSURLCacheStorageAllowedInMemoryOnly; [self.webView.mainFrame loadRequest:request]; } @@ -127,7 +132,7 @@ if ([savePanel runModal] == NSFileHandlingPanelOKButton) { NSLog(@"Will save CSV at: %@", savePanel.URL); - NSURL *url = [NSURL URLWithString:apiURL relativeToURL:[NSURL URLWithString:self.baseURL]]; + NSURL *url = [NSURL URLWithString:apiURL relativeToURL:[NSURL URLWithString:BaseURL()]]; NSURLRequest *csvRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:10.0f]; [NSURLConnection sendAsynchronousRequest:csvRequest queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { NSError *writeError = nil; @@ -161,10 +166,6 @@ #pragma mark - Getters / Setters -- (NSString *)baseURL { - return SettingsManager.instance.baseURL.length ? SettingsManager.instance.baseURL : LocalHostBaseURL(); -} - - (void)setLoading:(BOOL)loading { _loading = loading; @@ -204,10 +205,22 @@ - (IBAction)resetPassword:(id)sender { ResetPasswordWindowController *resetPasswordWindowController = [[ResetPasswordWindowController alloc] init]; + resetPasswordWindowController.delegate = self; [[NSApplication sharedApplication] runModalForWindow:resetPasswordWindowController.window]; } +#pragma mark - ResetPasswordWindowControllerDelegate + +- (void)resetPasswordWindowController:(ResetPasswordWindowController *)resetPasswordWindowController didFinishWithResetToken:(NSString *)resetToken { + NSString *passwordResetURLString = [NSString stringWithFormat:@"%@/auth/reset_password/%@", BaseURL(), resetToken]; + NSLog(@"Navigating to password reset URL: %@...", passwordResetURLString); + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:passwordResetURLString]]; + [self.webView.mainFrame loadRequest:request]; +} + + #pragma mark - WebResourceLoadDelegate - (void)webView:(WebView *)sender resource:(id)identifier didFinishLoadingFromDataSource:(WebDataSource *)dataSource { diff --git a/OSX/Metabase/UI/ResetPasswordWindowController.h b/OSX/Metabase/UI/ResetPasswordWindowController.h index c045c5e4ad8d81f72ac48572083fcf3771d93f52..c6fd88a530d9538d73266a73c05e088ab0cba95b 100644 --- a/OSX/Metabase/UI/ResetPasswordWindowController.h +++ b/OSX/Metabase/UI/ResetPasswordWindowController.h @@ -8,6 +8,15 @@ @import Cocoa; -@interface ResetPasswordWindowController : NSWindowController +@class ResetPasswordWindowController; + + +@protocol ResetPasswordWindowControllerDelegate <NSObject> +- (void)resetPasswordWindowController:(ResetPasswordWindowController *)resetPasswordWindowController didFinishWithResetToken:(NSString *)resetToken; +@end + + +@interface ResetPasswordWindowController : NSWindowController +@property (weak) id<ResetPasswordWindowControllerDelegate> delegate; @end diff --git a/OSX/Metabase/UI/ResetPasswordWindowController.m b/OSX/Metabase/UI/ResetPasswordWindowController.m index 49b70068d75373ab3432a01c00f770f4bc81f3d9..e5e5ec12f796598a4632754350ffade8efaa3856 100644 --- a/OSX/Metabase/UI/ResetPasswordWindowController.m +++ b/OSX/Metabase/UI/ResetPasswordWindowController.m @@ -6,12 +6,16 @@ // Copyright (c) 2015 Metabase. All rights reserved. // +#import <objc/runtime.h> + +#import "ResetPasswordTask.h" #import "ResetPasswordWindowController.h" @interface ResetPasswordWindowController () <NSTextFieldDelegate> @property (weak) IBOutlet NSButton *resetPasswordButton; @property (weak) IBOutlet NSTextField *emailAddressTextField; +@property (nonatomic, strong) ResetPasswordTask *resetPasswordTask; @end @@ -31,7 +35,29 @@ #pragma mark - Actions - (IBAction)resetPasswordButtonPressed:(NSButton *)sender { - NSLog(@"RESET PASSWORD!"); + self.resetPasswordButton.enabled = NO; + self.resetPasswordButton.title = @"One moment..."; + self.emailAddressTextField.enabled = NO; + + self.resetPasswordTask = [[ResetPasswordTask alloc] init]; + [self.resetPasswordTask resetPasswordForEmailAddress:self.emailAddressTextField.stringValue success:^(NSString *resetToken) { + self.emailAddressTextField.enabled = YES; + self.resetPasswordButton.title = @"Success!"; + + NSLog(@"Got reset token: '%@'", resetToken); + [self.delegate resetPasswordWindowController:self didFinishWithResetToken:resetToken]; + + } error:^(NSString *errorMessage) { + self.emailAddressTextField.enabled = YES; + self.resetPasswordButton.enabled = YES; + self.resetPasswordButton.title = @"Reset Password"; + + [[NSAlert alertWithMessageText:@"Password Reset Failed" defaultButton:@"Done" alternateButton:nil otherButton:nil informativeTextWithFormat:@"%@", errorMessage] runModal]; + }]; +} + +- (IBAction)emailAddressTextFieldDidReturn:(id)sender { + if (self.resetPasswordButton.isEnabled) [self resetPasswordButtonPressed:self.resetPasswordButton]; } diff --git a/OSX/Metabase/UI/ResetPasswordWindowController.xib b/OSX/Metabase/UI/ResetPasswordWindowController.xib index cbb8e9e39fd9e33f84771505b31123375a2dea8f..c324adae9006a2fe41750ed41957de231885a1ce 100644 --- a/OSX/Metabase/UI/ResetPasswordWindowController.xib +++ b/OSX/Metabase/UI/ResetPasswordWindowController.xib @@ -8,30 +8,22 @@ <connections> <outlet property="emailAddressTextField" destination="SGZ-eW-nZQ" id="aMf-FE-ey1"/> <outlet property="resetPasswordButton" destination="1wF-h5-c8o" id="N1J-e1-a4y"/> + <outlet property="window" destination="FU9-gs-izu" id="SSm-mw-LIb"/> </connections> </customObject> <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> <customObject id="-3" userLabel="Application"/> - <window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" animationBehavior="default" id="FU9-gs-izu"> + <window title="Reset Password" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" animationBehavior="default" id="FU9-gs-izu"> <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/> <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> - <rect key="contentRect" x="283" y="305" width="492" height="242"/> + <rect key="contentRect" x="283" y="305" width="492" height="229"/> <rect key="screenRect" x="0.0" y="0.0" width="2560" height="1418"/> <view key="contentView" id="3rs-Tz-giP"> - <rect key="frame" x="0.0" y="0.0" width="492" height="242"/> + <rect key="frame" x="0.0" y="0.0" width="492" height="229"/> <autoresizingMask key="autoresizingMask"/> <subviews> - <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QaQ-Qx-Ypt"> - <rect key="frame" x="185" y="207" width="122" height="19"/> - <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> - <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" enabled="NO" refusesFirstResponder="YES" sendsActionOnEndEditing="YES" title="Reset Password" id="Kcn-M1-bVY"> - <font key="font" metaFont="system" size="16"/> - <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> - <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> - </textFieldCell> - </textField> <textField horizontalHuggingPriority="249" verticalHuggingPriority="249" horizontalCompressionResistancePriority="240" verticalCompressionResistancePriority="800" translatesAutoresizingMaskIntoConstraints="NO" id="oEx-8w-YG4"> - <rect key="frame" x="14" y="157" width="464" height="34"/> + <rect key="frame" x="14" y="175" width="464" height="34"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <constraints> <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="460" id="qwI-wW-wTd"/> @@ -43,7 +35,7 @@ </textFieldCell> </textField> <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1wF-h5-c8o"> - <rect key="frame" x="178" y="9" width="136" height="32"/> + <rect key="frame" x="178" y="13" width="136" height="32"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <buttonCell key="cell" type="push" title="Reset Password" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="YII-IA-Kyg"> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> @@ -54,7 +46,7 @@ </connections> </button> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8ft-6m-5T0"> - <rect key="frame" x="200" y="108" width="92" height="17"/> + <rect key="frame" x="200" y="126" width="92" height="17"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" enabled="NO" refusesFirstResponder="YES" sendsActionOnEndEditing="YES" title="Email Address" id="ETj-8f-dsZ"> <font key="font" metaFont="system"/> @@ -63,17 +55,18 @@ </textFieldCell> </textField> <textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SGZ-eW-nZQ"> - <rect key="frame" x="118" y="78" width="256" height="22"/> + <rect key="frame" x="118" y="96" width="256" height="22"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <constraints> <constraint firstAttribute="width" constant="256" id="8Fr-91-TXf"/> </constraints> - <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" state="on" borderStyle="bezel" placeholderString="cam@toucans4lyfe.org" drawsBackground="YES" id="mP8-DI-PfW"> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" state="on" borderStyle="bezel" alignment="center" placeholderString="cam@toucans4lyfe.org" drawsBackground="YES" id="mP8-DI-PfW"> <font key="font" metaFont="system"/> <color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> </textFieldCell> <connections> + <action selector="emailAddressTextFieldDidReturn:" target="-2" id="JLo-r8-R9z"/> <outlet property="delegate" destination="-2" id="h0P-Zb-hJJ"/> </connections> </textField> @@ -85,13 +78,11 @@ <constraint firstItem="1wF-h5-c8o" firstAttribute="top" relation="greaterThanOrEqual" secondItem="SGZ-eW-nZQ" secondAttribute="bottom" constant="32" id="CM1-Nh-mnk"/> <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="SGZ-eW-nZQ" secondAttribute="trailing" constant="16" id="NHO-1C-XOJ"/> <constraint firstItem="SGZ-eW-nZQ" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="3rs-Tz-giP" secondAttribute="leading" constant="16" id="On6-0w-5PF"/> - <constraint firstAttribute="bottom" secondItem="1wF-h5-c8o" secondAttribute="bottom" constant="16" id="VRO-Qm-4vb"/> - <constraint firstItem="oEx-8w-YG4" firstAttribute="top" secondItem="QaQ-Qx-Ypt" secondAttribute="bottom" constant="16" id="bMB-cF-PJe"/> + <constraint firstAttribute="bottom" secondItem="1wF-h5-c8o" secondAttribute="bottom" constant="20" id="VRO-Qm-4vb"/> + <constraint firstItem="oEx-8w-YG4" firstAttribute="top" secondItem="3rs-Tz-giP" secondAttribute="top" constant="20" id="bzN-6L-86m"/> <constraint firstItem="SGZ-eW-nZQ" firstAttribute="top" secondItem="8ft-6m-5T0" secondAttribute="bottom" constant="8" id="e7T-c5-OYs"/> - <constraint firstAttribute="centerX" secondItem="QaQ-Qx-Ypt" secondAttribute="centerX" id="kFE-8l-poc"/> - <constraint firstAttribute="centerY" secondItem="SGZ-eW-nZQ" secondAttribute="centerY" constant="-32" id="mTu-yO-Smv"/> + <constraint firstAttribute="centerY" secondItem="SGZ-eW-nZQ" secondAttribute="centerY" constant="-8" id="mTu-yO-Smv"/> <constraint firstAttribute="trailing" secondItem="oEx-8w-YG4" secondAttribute="trailing" constant="16" id="shZ-a7-r8y"/> - <constraint firstItem="QaQ-Qx-Ypt" firstAttribute="top" secondItem="3rs-Tz-giP" secondAttribute="top" constant="16" id="uRv-1M-0Si"/> <constraint firstAttribute="centerX" secondItem="1wF-h5-c8o" secondAttribute="centerX" id="vH7-Xk-LcO"/> <constraint firstAttribute="centerX" secondItem="8ft-6m-5T0" secondAttribute="centerX" id="vh8-pN-ohL"/> </constraints> diff --git a/bin/osx-setup b/bin/osx-setup index 3b433bca7125ba9ccf039f19c41cbefabb9bc722..66e1f8e6b4c8bcd467627d01c1a16800afefcdb3 100755 --- a/bin/osx-setup +++ b/bin/osx-setup @@ -18,6 +18,9 @@ use constant JRE_HOME => trim(`/usr/libexec/java_home`) . '/jre'; use constant UBERJAR_SRC => getcwd() . '/target/uberjar/metabase.jar'; use constant UBERJAR_DEST => getcwd() . '/OSX/Resources/metabase.jar'; +use constant RESET_PW_SRC => getcwd() . '/reset-password-artifacts/reset-password/reset-password.jar'; +use constant RESET_PW_DEST => getcwd() . '/OSX/Resources/reset-password.jar'; + use constant ENABLE_JAR_PACKING => 0; use constant ENABLE_JAR_OPTIONAL_FILE_STRIPPING => 0; @@ -76,11 +79,17 @@ remove_jre_optional_files() if ENABLE_JAR_OPTIONAL_FILE_STRIPPING; # Pack JARs in JRE if applicable pack_jar() if ENABLE_JAR_PACKING; -# Build uberjar if needed +# Build jars if needed (system('./bin/build') or die $!) unless -f UBERJAR_SRC; +(system('lein', 'with-profile', 'reset-password', 'jar') or die $!) unless -f RESET_PW_SRC; -# Copy uberjar over -announce 'Copying uberjar ' . UBERJAR_SRC . ' -> ' . UBERJAR_DEST; -copy(get_file_or_die(UBERJAR_SRC), UBERJAR_DEST) or die $!; +# Copy jars over +sub copy_jar { + my ($src, $dest) = @_; + announce 'Copying jar ' . $src . ' -> ' . $dest; + copy(get_file_or_die($src), $dest) or die $!; +} +copy_jar(UBERJAR_SRC, UBERJAR_DEST); +copy_jar(RESET_PW_SRC, RESET_PW_DEST); print_giant_success_banner(); diff --git a/project.clj b/project.clj index d296e19aec52706f420d29c0f4cfe08c2b178b3e..6f1f7e4bfc3e448608f1e25094ef6400e346d0b3 100644 --- a/project.clj +++ b/project.clj @@ -100,4 +100,14 @@ [incanter/incanter-core "1.9.0"]] ; Satistical functions like normal distibutions}}) :source-paths ["sample_dataset"] :global-vars {*warn-on-reflection* false} - :main ^:skip-aot metabase.sample-dataset.generate}}) + :main ^:skip-aot metabase.sample-dataset.generate} + ;; Run reset password from source: lein with-profile reset-password run /path/to/metabase.db email@address.com + ;; Create the reset password JAR: lein with-profile reset-password jar + ;; -> ./reset-password-artifacts/reset-password/reset-password.jar + ;; Run the reset password JAR: java -classpath /path/to/metabase-uberjar.jar:/path/to/reset-password.jar \ + ;; metabase.reset_password.core /path/to/metabase.db email@address.com + :reset-password {:source-paths ["reset_password"] + :global-vars {*warn-on-reflection* false} + :main metabase.reset-password.core + :jar-name "reset-password.jar" + :target-path "reset-password-artifacts/%s"}}) ; different than ./target because otherwise lein uberjar will delete our artifacts and vice versa diff --git a/reset_password/metabase/reset_password/core.clj b/reset_password/metabase/reset_password/core.clj new file mode 100644 index 0000000000000000000000000000000000000000..d931151ce205557c7b1c366e60fb99b6dbdee022 --- /dev/null +++ b/reset_password/metabase/reset_password/core.clj @@ -0,0 +1,23 @@ +(ns metabase.reset-password.core + (:gen-class) + (:require [clojure.java.jdbc :as jdbc])) + +(defn- db-filepath->connection-details [filepath] + {:classname "org.h2.Driver" + :subprotocol "h2" + :subname (str "file:" filepath ";MV_STORE=FALSE;AUTO_SERVER=TRUE;DB_CLOSE_DELAY=-1")}) + +(defn- set-reset-token! [dbpath email-address reset-token] + (let [rows-affected (jdbc/execute! (db-filepath->connection-details dbpath) ["UPDATE CORE_USER SET RESET_TOKEN = ?, RESET_TRIGGERED = ? WHERE EMAIL = ?;" reset-token (System/currentTimeMillis) email-address])] + (when (not= rows-affected [1]) + (throw (Exception. (format "No user found with email address '%s'. Please check the spelling and try again." email-address)))))) + +(defn -main + [dbpath email-address] + (try + (let [reset-token (str (java.util.UUID/randomUUID))] + (set-reset-token! dbpath email-address reset-token) + (println (format "OK [[[%s]]]" reset-token))) + (catch Throwable e + (println (format "FAIL [[[%s]]]" (.getMessage e))) + (System/exit -1)))) diff --git a/src/metabase/db.clj b/src/metabase/db.clj index 1b322ad8c53ab9fa51ab2e262a6c872e7f9ae8b4..0beed2168090bf7a1218f52cbe15bc774938647f 100644 --- a/src/metabase/db.clj +++ b/src/metabase/db.clj @@ -81,9 +81,9 @@ {:pre [(map? db-details)]} ;; TODO: it's probably a good idea to put some more validation here and be really strict about what's in `db-details` (case (:type db-details) - :h2 (kdb/h2 (assoc db-details :naming {:keys s/lower-case - :fields s/upper-case})) - :mysql (kdb/mysql (assoc db-details :db (:dbname db-details))) + :h2 (kdb/h2 (assoc db-details :naming {:keys s/lower-case + :fields s/upper-case})) + :mysql (kdb/mysql (assoc db-details :db (:dbname db-details))) :postgres (kdb/postgres (assoc db-details :db (:dbname db-details)))))