mirror of https://github.com/mitchell/selfpass.git
Add credential screen & screen arguments; refactor config screen
This commit is contained in:
parent
af8834f7bd
commit
910bdeae12
|
@ -5,11 +5,13 @@ import 'repositories/credentials_client.dart';
|
||||||
import 'repositories/config.dart' as repo;
|
import 'repositories/config.dart' as repo;
|
||||||
|
|
||||||
import 'screens/authentication.dart';
|
import 'screens/authentication.dart';
|
||||||
|
import 'screens/credential.dart';
|
||||||
import 'screens/credentials.dart';
|
import 'screens/credentials.dart';
|
||||||
import 'screens/config.dart';
|
import 'screens/config.dart';
|
||||||
import 'screens/home.dart';
|
import 'screens/home.dart';
|
||||||
|
|
||||||
import 'types/abstracts.dart';
|
import 'types/abstracts.dart';
|
||||||
|
import 'types/screen_arguments.dart';
|
||||||
|
|
||||||
void main() => runApp(Selfpass());
|
void main() => runApp(Selfpass());
|
||||||
|
|
||||||
|
@ -31,23 +33,37 @@ class Selfpass extends StatelessWidget {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '/home':
|
case '/home':
|
||||||
title = 'Credential Hosts';
|
title = 'Hosts';
|
||||||
builder = (BuildContext context) => Provider<CredentialsRepo>(
|
builder = (BuildContext context) => Provider<CredentialsRepo>(
|
||||||
builder: (BuildContext context) =>
|
builder: (BuildContext context) =>
|
||||||
CredentialsClient(settings.arguments),
|
CredentialsClient.cached(config: settings.arguments),
|
||||||
child: Home(),
|
child: Home(),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '/credentials':
|
case '/credentials':
|
||||||
title = 'Credentials';
|
title = 'Credentials';
|
||||||
builder =
|
builder = (BuildContext context) => Provider<CredentialsRepo>(
|
||||||
(BuildContext context) => Credentials(settings.arguments);
|
builder: (BuildContext context) =>
|
||||||
|
CredentialsClient.cached(),
|
||||||
|
child: Credentials(settings.arguments),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '/credential':
|
||||||
|
title = 'Credential';
|
||||||
|
builder = (BuildContext context) => Provider<CredentialsRepo>(
|
||||||
|
builder: (BuildContext context) =>
|
||||||
|
CredentialsClient.cached(),
|
||||||
|
child: Credential(settings.arguments),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '/config':
|
case '/config':
|
||||||
|
final ConfigScreenArguments arguments = settings.arguments;
|
||||||
title = 'Configuration';
|
title = 'Configuration';
|
||||||
builder = (BuildContext context) => Config(settings.arguments);
|
builder = (BuildContext context) =>
|
||||||
|
Config(arguments.connectionConfig, arguments.privateKey);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import '../types/connection_config.dart';
|
||||||
import '../types/credential.dart';
|
import '../types/credential.dart';
|
||||||
|
|
||||||
class CredentialsClient implements CredentialsRepo {
|
class CredentialsClient implements CredentialsRepo {
|
||||||
|
static CredentialsClient _cached;
|
||||||
grpc.CredentialServiceClient _client;
|
grpc.CredentialServiceClient _client;
|
||||||
|
|
||||||
CredentialsClient(ConnectionConfig config) {
|
CredentialsClient(ConnectionConfig config) {
|
||||||
|
@ -32,6 +33,9 @@ class CredentialsClient implements CredentialsRepo {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
factory CredentialsClient.cached({ConnectionConfig config}) =>
|
||||||
|
_cached == null ? _cached = CredentialsClient(config) : _cached;
|
||||||
|
|
||||||
Stream<Metadata> getAllMetadata(String sourceHost) {
|
Stream<Metadata> getAllMetadata(String sourceHost) {
|
||||||
final request = grpc.GetAllMetadataRequest();
|
final request = grpc.GetAllMetadataRequest();
|
||||||
request.sourceHost = sourceHost;
|
request.sourceHost = sourceHost;
|
||||||
|
|
|
@ -48,6 +48,7 @@ class _AuthenticationState extends State<Authentication> {
|
||||||
const Flexible(child: Text('Master password:')),
|
const Flexible(child: Text('Master password:')),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
maxLines: 1,
|
||||||
obscure: true,
|
obscure: true,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
|
@ -59,6 +60,7 @@ class _AuthenticationState extends State<Authentication> {
|
||||||
children.add(const Flexible(child: Text('Re-enter password:')));
|
children.add(const Flexible(child: Text('Re-enter password:')));
|
||||||
children.add(Flexible(
|
children.add(Flexible(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
maxLines: 1,
|
||||||
obscure: true,
|
obscure: true,
|
||||||
controller: _confirmController,
|
controller: _confirmController,
|
||||||
),
|
),
|
||||||
|
@ -85,7 +87,7 @@ class _AuthenticationState extends State<Authentication> {
|
||||||
|
|
||||||
children.add(CupertinoButton(
|
children.add(CupertinoButton(
|
||||||
child: Text('Enter'),
|
child: Text('Enter'),
|
||||||
onPressed: _buildSubmitPressedBuilder(context),
|
onPressed: _buildEnterPressedBuilder(context),
|
||||||
));
|
));
|
||||||
|
|
||||||
if (_passesDontMatch) {
|
if (_passesDontMatch) {
|
||||||
|
@ -99,7 +101,7 @@ class _AuthenticationState extends State<Authentication> {
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
VoidCallback _buildSubmitPressedBuilder(BuildContext context) {
|
VoidCallback _buildEnterPressedBuilder(BuildContext context) {
|
||||||
return () async {
|
return () async {
|
||||||
if (await _passwordIsSet) {
|
if (await _passwordIsSet) {
|
||||||
if (await _config.matchesPasswordHash(_passwordController.text)) {
|
if (await _config.matchesPasswordHash(_passwordController.text)) {
|
||||||
|
|
|
@ -8,11 +8,13 @@ import '../widgets/text_field.dart';
|
||||||
|
|
||||||
class Config extends StatefulWidget {
|
class Config extends StatefulWidget {
|
||||||
final ConnectionConfig connectionConfig;
|
final ConnectionConfig connectionConfig;
|
||||||
|
final String privateKey;
|
||||||
|
|
||||||
const Config(this.connectionConfig, {Key key}) : super(key: key);
|
const Config(this.connectionConfig, this.privateKey, {Key key})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State createState() => _ConfigState(this.connectionConfig);
|
State createState() => _ConfigState(this.connectionConfig, this.privateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ConfigState extends State<Config> {
|
class _ConfigState extends State<Config> {
|
||||||
|
@ -20,10 +22,12 @@ class _ConfigState extends State<Config> {
|
||||||
TextEditingController _caCertController;
|
TextEditingController _caCertController;
|
||||||
TextEditingController _certController;
|
TextEditingController _certController;
|
||||||
TextEditingController _privateCertController;
|
TextEditingController _privateCertController;
|
||||||
|
TextEditingController _privateKeyController;
|
||||||
ConnectionConfig _connectionConfig;
|
ConnectionConfig _connectionConfig;
|
||||||
|
String _privateKey;
|
||||||
ConfigRepo _config;
|
ConfigRepo _config;
|
||||||
|
|
||||||
_ConfigState(this._connectionConfig) {
|
_ConfigState(this._connectionConfig, this._privateKey) {
|
||||||
if (_connectionConfig == null) {
|
if (_connectionConfig == null) {
|
||||||
_connectionConfig = ConnectionConfig();
|
_connectionConfig = ConnectionConfig();
|
||||||
}
|
}
|
||||||
|
@ -35,6 +39,8 @@ class _ConfigState extends State<Config> {
|
||||||
TextEditingController(text: _connectionConfig.caCertificate);
|
TextEditingController(text: _connectionConfig.caCertificate);
|
||||||
_privateCertController =
|
_privateCertController =
|
||||||
TextEditingController(text: _connectionConfig.privateCertificate);
|
TextEditingController(text: _connectionConfig.privateCertificate);
|
||||||
|
|
||||||
|
_privateKeyController = TextEditingController(text: _privateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -57,22 +63,26 @@ class _ConfigState extends State<Config> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 50.0),
|
margin: const EdgeInsets.symmetric(horizontal: 30),
|
||||||
child: Column(children: [
|
child: ListView(children: [
|
||||||
Spacer(flex: 3),
|
Container(margin: EdgeInsets.only(top: 10), child: Text('Host:')),
|
||||||
Flexible(child: Text('Host:')),
|
TextField(maxLines: 1, controller: _hostController),
|
||||||
Flexible(child: TextField(maxLines: 1, controller: _hostController)),
|
Container(
|
||||||
Flexible(child: Text('CA certificate:')),
|
margin: EdgeInsets.only(top: 5), child: Text('Private key:')),
|
||||||
Flexible(
|
TextField(maxLines: 1, controller: _privateKeyController),
|
||||||
child: TextField(maxLines: 3, controller: _caCertController)),
|
Container(
|
||||||
Flexible(child: Text('Client certificate:')),
|
margin: EdgeInsets.only(top: 5), child: Text('CA certificate:')),
|
||||||
Flexible(child: TextField(maxLines: 3, controller: _certController)),
|
TextField(maxLines: 5, controller: _caCertController),
|
||||||
Flexible(child: Text('Private certificate:')),
|
Container(
|
||||||
Flexible(
|
margin: EdgeInsets.only(top: 5),
|
||||||
child:
|
child: Text('Client certificate:')),
|
||||||
TextField(maxLines: 3, controller: _privateCertController)),
|
TextField(maxLines: 5, controller: _certController),
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(top: 5),
|
||||||
|
child: Text('Private certificate:')),
|
||||||
|
TextField(maxLines: 5, controller: _privateCertController),
|
||||||
CupertinoButton(
|
CupertinoButton(
|
||||||
child: Text('Save'), onPressed: _makeSaveOnPressed(context))
|
child: Text('Save'), onPressed: _makeSaveOnPressed(context)),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -116,6 +126,7 @@ class _ConfigState extends State<Config> {
|
||||||
);
|
);
|
||||||
|
|
||||||
await _config.setConnectionConfig(connConfig);
|
await _config.setConnectionConfig(connConfig);
|
||||||
|
await _config.setPrivateKey(_privateKeyController.text);
|
||||||
|
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
.pushReplacementNamed('/home', arguments: connConfig);
|
.pushReplacementNamed('/home', arguments: connConfig);
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../types/credential.dart' as types;
|
||||||
|
|
||||||
|
import '../widgets/text_field.dart';
|
||||||
|
|
||||||
|
class Credential extends StatefulWidget {
|
||||||
|
final types.Credential credential;
|
||||||
|
|
||||||
|
const Credential(this.credential, {Key key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State createState() => _CredentialState(credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CredentialState extends State<Credential> {
|
||||||
|
_CredentialControllers _controllers;
|
||||||
|
Map<String, _FieldBuildConfig> _fieldMap;
|
||||||
|
types.Credential _credential;
|
||||||
|
|
||||||
|
_CredentialState(this._credential) : super() {
|
||||||
|
_controllers = _CredentialControllers.fromCredential(_credential);
|
||||||
|
_fieldMap = _buildFieldMap(_controllers, _credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CupertinoPageScaffold(
|
||||||
|
navigationBar: CupertinoNavigationBar(),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(top: 30, left: 30),
|
||||||
|
child: ListView(
|
||||||
|
children: _buildFieldRows(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, _FieldBuildConfig> _buildFieldMap(
|
||||||
|
_CredentialControllers controllers,
|
||||||
|
types.Credential credential,
|
||||||
|
) {
|
||||||
|
final fieldMap = {
|
||||||
|
'Id:': _FieldBuildConfig(mutable: false, text: credential.meta.id),
|
||||||
|
'Created: ': _FieldBuildConfig(
|
||||||
|
mutable: false,
|
||||||
|
copyable: false,
|
||||||
|
text: credential.meta.createdAt.toString(),
|
||||||
|
),
|
||||||
|
'Updated: ': _FieldBuildConfig(
|
||||||
|
mutable: false,
|
||||||
|
copyable: false,
|
||||||
|
text: credential.meta.updatedAt.toString(),
|
||||||
|
),
|
||||||
|
'Host:': _FieldBuildConfig(controller: controllers.sourceHost),
|
||||||
|
'Primary:': _FieldBuildConfig(controller: controllers.primary),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (credential.meta.tag != null && credential.meta.tag != '') {
|
||||||
|
fieldMap['Tag'] = _FieldBuildConfig(controller: controllers.tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential.username != null && credential.username != '') {
|
||||||
|
fieldMap['User:'] = _FieldBuildConfig(controller: controllers.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential.email != null && credential.email != '') {
|
||||||
|
fieldMap['Email:'] = _FieldBuildConfig(controller: controllers.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildFieldRows(BuildContext context) {
|
||||||
|
List<Widget> rows = [];
|
||||||
|
|
||||||
|
_fieldMap.forEach((key, value) {
|
||||||
|
rows.add(Container(
|
||||||
|
margin: EdgeInsets.only(top: 10),
|
||||||
|
child: Text(key, style: TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
));
|
||||||
|
|
||||||
|
final List<Widget> widgets = [
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: value.mutable
|
||||||
|
? TextField(maxLines: 1, controller: value.controller)
|
||||||
|
: Container(
|
||||||
|
margin: EdgeInsets.symmetric(vertical: 10),
|
||||||
|
child: Text(value.text),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (value.copyable) {
|
||||||
|
widgets.add(Flexible(
|
||||||
|
child: CupertinoButton(
|
||||||
|
child: Text('Copy'),
|
||||||
|
onPressed: () => Clipboard.setData(ClipboardData(
|
||||||
|
text: value.mutable ? value.controller.text : value.text,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.add(Row(children: widgets));
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FieldBuildConfig {
|
||||||
|
final TextEditingController controller;
|
||||||
|
final String text;
|
||||||
|
final bool mutable;
|
||||||
|
final bool copyable;
|
||||||
|
|
||||||
|
const _FieldBuildConfig({
|
||||||
|
this.mutable = true,
|
||||||
|
this.copyable = true,
|
||||||
|
this.controller,
|
||||||
|
this.text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CredentialControllers {
|
||||||
|
final TextEditingController sourceHost;
|
||||||
|
final TextEditingController primary;
|
||||||
|
final TextEditingController tag;
|
||||||
|
final TextEditingController username;
|
||||||
|
final TextEditingController email;
|
||||||
|
final TextEditingController password;
|
||||||
|
final TextEditingController otpSecret;
|
||||||
|
|
||||||
|
const _CredentialControllers({
|
||||||
|
this.sourceHost,
|
||||||
|
this.primary,
|
||||||
|
this.tag,
|
||||||
|
this.username,
|
||||||
|
this.email,
|
||||||
|
this.password,
|
||||||
|
this.otpSecret,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory _CredentialControllers.fromCredential(types.Credential credential) =>
|
||||||
|
_CredentialControllers(
|
||||||
|
sourceHost: TextEditingController(text: credential.meta.sourceHost),
|
||||||
|
primary: TextEditingController(text: credential.meta.primary),
|
||||||
|
tag: TextEditingController(text: credential.meta.tag),
|
||||||
|
username: TextEditingController(text: credential.username),
|
||||||
|
email: TextEditingController(text: credential.email),
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../types/abstracts.dart';
|
||||||
import '../types/credential.dart';
|
import '../types/credential.dart';
|
||||||
|
|
||||||
import '../widgets/tappable_text_list.dart';
|
import '../widgets/tappable_text_list.dart';
|
||||||
|
@ -18,12 +20,21 @@ class Credentials extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, GestureTapCallback> _buildTappableText(BuildContext context) {
|
Map<String, GestureTapCallback> _buildTappableText(BuildContext context) {
|
||||||
var handleOnTap = () {};
|
var makeOnTapHandler = (String id) => () async {
|
||||||
|
final credential =
|
||||||
|
await Provider.of<CredentialsRepo>(context).get(id);
|
||||||
|
Navigator.of(context).pushNamed('/credential', arguments: credential);
|
||||||
|
};
|
||||||
|
|
||||||
Map<String, GestureTapCallback> tappableText = {};
|
Map<String, GestureTapCallback> tappableText = {};
|
||||||
|
|
||||||
metadatas.forEach(
|
metadatas.forEach((Metadata metadata) {
|
||||||
(Metadata metadata) => tappableText[metadata.id] = handleOnTap);
|
var primary = metadata.primary;
|
||||||
|
if (metadata.tag != null && metadata.tag != '') {
|
||||||
|
primary += "-" + metadata.tag;
|
||||||
|
}
|
||||||
|
tappableText[primary] = makeOnTapHandler(metadata.id);
|
||||||
|
});
|
||||||
|
|
||||||
return tappableText;
|
return tappableText;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../types/abstracts.dart';
|
import '../types/abstracts.dart';
|
||||||
import '../types/credential.dart';
|
import '../types/credential.dart';
|
||||||
|
import '../types/screen_arguments.dart';
|
||||||
|
|
||||||
import '../widgets/tappable_text_list.dart';
|
import '../widgets/tappable_text_list.dart';
|
||||||
|
|
||||||
|
@ -72,7 +73,7 @@ class _HomeState extends State<Home> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final handleOnTap = (List<Metadata> metadatas) => () =>
|
final handleOnTap = (List<Metadata> metadatas) => () async =>
|
||||||
Navigator.of(context).pushNamed('/credentials', arguments: metadatas);
|
Navigator.of(context).pushNamed('/credentials', arguments: metadatas);
|
||||||
|
|
||||||
final Map<String, GestureTapCallback> tappableText = {};
|
final Map<String, GestureTapCallback> tappableText = {};
|
||||||
|
@ -88,7 +89,8 @@ class _HomeState extends State<Home> {
|
||||||
}
|
}
|
||||||
|
|
||||||
GestureTapCallback _makeConfigOnTapHandler(BuildContext context) {
|
GestureTapCallback _makeConfigOnTapHandler(BuildContext context) {
|
||||||
return () async => Navigator.of(context)
|
return () async => Navigator.of(context).pushNamed('/config',
|
||||||
.pushNamed('/config', arguments: await _config.connectionConfig);
|
arguments: ConfigScreenArguments(
|
||||||
|
await _config.connectionConfig, await _config.privateKey));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import 'connection_config.dart';
|
||||||
|
|
||||||
|
class ConfigScreenArguments {
|
||||||
|
final ConnectionConfig connectionConfig;
|
||||||
|
final String privateKey;
|
||||||
|
|
||||||
|
const ConfigScreenArguments(this.connectionConfig, this.privateKey);
|
||||||
|
}
|
|
@ -7,6 +7,8 @@ typedef OnSubmittedBuilder = ValueChanged<String> Function(
|
||||||
class TextField extends StatelessWidget {
|
class TextField extends StatelessWidget {
|
||||||
final OnSubmittedBuilder onSubmittedBuilder;
|
final OnSubmittedBuilder onSubmittedBuilder;
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
|
final Widget prefix;
|
||||||
|
final Widget suffix;
|
||||||
final bool obscure;
|
final bool obscure;
|
||||||
final bool autofocus;
|
final bool autofocus;
|
||||||
final bool autocorrect;
|
final bool autocorrect;
|
||||||
|
@ -21,6 +23,8 @@ class TextField extends StatelessWidget {
|
||||||
this.minLines,
|
this.minLines,
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
this.autocorrect = false,
|
this.autocorrect = false,
|
||||||
|
this.prefix,
|
||||||
|
this.suffix,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -43,6 +47,8 @@ class TextField extends StatelessWidget {
|
||||||
autocorrect: autocorrect,
|
autocorrect: autocorrect,
|
||||||
minLines: minLines,
|
minLines: minLines,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
|
prefix: prefix,
|
||||||
|
suffix: suffix,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue