user_view.dart 9.5 KB
Newer Older
1
2
3
4
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';

Leon Tappe's avatar
Leon Tappe committed
5
import 'package:digital_3g_common/backend_switcher.dart';
Leon Tappe's avatar
Leon Tappe committed
6
import 'package:digital_3g_common/ui.dart';
7
8
9
import 'package:encrypt/encrypt.dart';
import 'package:encrypt/encrypt_io.dart';
import 'package:flutter/material.dart';
Leon Tappe's avatar
Leon Tappe committed
10
import 'package:flutter_bloc/flutter_bloc.dart';
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pointycastle/asymmetric/api.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';

import '../api/auth_api.dart';
import '../api/user_api.dart';

class FormData {
  DateTime? certificationDate;
  TimeOfDay? certificationTime;
  bool isTested;
  bool isRecovered;
  bool isVaccinated;

  FormData({
    this.certificationDate,
    this.certificationTime,
    this.isTested = false,
    this.isRecovered = false,
    this.isVaccinated = false,
  });

  void resetBools() {
    isTested = false;
    isRecovered = false;
    isVaccinated = false;
  }
}

class UserView extends StatefulWidget {
  const UserView({key}) : super(key: key);

  @override
  _UserViewState createState() => _UserViewState();
}

class _UserViewState extends State<UserView> {
  final FormData _formData = FormData(
    certificationDate: DateTime.now(),
    certificationTime: TimeOfDay.now(),
  );

  final DateFormat _dateFormat = DateFormat.yMd();
  final DateFormat _timeFormat = DateFormat.Hm();

  String _expiryText = '';

  http.Client? _client;

  UserApi? _userApi;

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        PaddedCard(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text('Datenaufnahme', style: Theme.of(context).textTheme.headline5),
              CheckboxListTile(
                title: const Text('Getestet'),
                value: _formData.isTested,
                onChanged: (bool? input) {
                  setState(() {
                    _formData.resetBools();
                    _formData.isTested = input ?? false;
                    _expiryText = _makeExpiryString();
                  });
                },
              ),
              CheckboxListTile(
                title: const Text('Genesen'),
                value: _formData.isRecovered,
                onChanged: (bool? input) {
                  setState(() {
                    _formData.resetBools();
                    _formData.isRecovered = input ?? false;
                    _expiryText = _makeExpiryString();
                  });
                },
              ),
              CheckboxListTile(
                title: const Text('Geimpft'),
                value: _formData.isVaccinated,
                onChanged: (bool? input) {
                  setState(() {
                    _formData.resetBools();
                    _formData.isVaccinated = input ?? false;
                    _expiryText = _makeExpiryString();
                  });
                },
              ),
              const Divider(),
              ListTile(
                  title: const Text('Nachweisdatum'),
                  trailing: Text(_dateFormat.format(_formData.certificationDate ?? DateTime.now())),
                  onTap: () async {
                    final date = await showDatePicker(
                      context: context,
Leon Tappe's avatar
Leon Tappe committed
114
115
116
117
118
                        cancelText: 'Abbrechen',
                        confirmText: 'Bestätigen',
                        fieldLabelText: 'Datum',
                        fieldHintText: 'z.B. 01/04/2021',
                        helpText: 'Datum einstellen',
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
                      initialDate: _formData.certificationDate ?? DateTime.now(),
                      firstDate: DateTime.now().subtract(const Duration(days: 1000)),
                      lastDate: DateTime.now(),
                    );
                    if (date != null) {
                      setState(() {
                        _formData.certificationDate = date;
                        _expiryText = _makeExpiryString();
                      });
                    }
                  }),
              if (_formData.isTested)
                ListTile(
                    title: const Text('Nachweiszeit'),
                    trailing: Text(_formData.certificationTime?.format(context) ??
                        _timeFormat.format(DateTime.now())),
                    onTap: () async {
                      final time = await showTimePicker(
Leon Tappe's avatar
Leon Tappe committed
137
138
139
140
141
142
                        cancelText: 'Abbrechen',
                        confirmText: 'Bestätigen',
                        minuteLabelText: 'Minuten',
                        hourLabelText: 'Stunden',
                        helpText: 'Uhrzeit auswählen',
                        initialEntryMode: TimePickerEntryMode.dial,
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
                        context: context,
                        initialTime: _formData.certificationTime ?? TimeOfDay.now(),
                      );
                      if (time != null) {
                        setState(() {
                          _formData.certificationTime = time;
                          _expiryText = _makeExpiryString();
                        });
                      }
                    }),
              const Divider(),
              const Text('Gültig bis:'),
              Text(_expiryText, style: Theme.of(context).textTheme.headline6),
            ],
          ),
        ),
        const Divider(),
        Padding(
          padding: const EdgeInsets.all(16.0),
          child: ElevatedButton(
            onPressed: _onShowCode,
            child: const Text('QR-Code zeigen'),
          ),
        ),
      ],
    );
  }

  @override
  void didChangeDependencies() {
    _client = Provider.of<AuthApi>(context, listen: false).client;
Leon Tappe's avatar
Leon Tappe committed
174
    _userApi = UserApi(_client!, BlocProvider.of<BackendSwitcher>(context).state);
175
176
177
178
179
180
181
182
183
184
185
186
187
188
    super.didChangeDependencies();
  }

  @override
  void initState() {
    super.initState();
    _initKey();
    _expiryText = _makeExpiryString();
  }

  DateTime? _calculateExpiryDate(FormData form) {
    var date = form.certificationDate;
    var time = form.certificationTime;
    if (form.isTested) {
Leon Tappe's avatar
Leon Tappe committed
189
190
      date = DateTime(date!.year, date.month, date.day).add(const Duration(hours: 48));
      date = date.add(Duration(hours: time!.hour, minutes: time.minute));
191
192
193
194
195
196
197
198
199
200
201
202
203
    } else if (form.isRecovered) {
      date = date!.add(const Duration(days: 183));
      date = DateTime(date.year, date.month, date.day, 23, 59);
    } else if (form.isVaccinated) {
      date = date!.add(const Duration(days: 365));
      date = DateTime(date.year, date.month, date.day, 23, 59);
    } else {
      return null;
    }
    return date;
  }

  Future<Uint8List?> _getPublicKey() async {
Leon Tappe's avatar
Leon Tappe committed
204
205
    final response =
        await _client!.get(Uri.parse('${BlocProvider.of<BackendSwitcher>(context).state}/pubkey'));
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270

    if (response.statusCode == 200) {
      return response.bodyBytes;
    } else {
      return null;
    }
  }

  Future<void> _initKey() async {
    if (Provider.of<AuthApi>(context, listen: false).user?.credentials == null) return;

    final basePath = (await getApplicationDocumentsDirectory()).path;
    final keyFile = File('$basePath/public_key.pem');

    if (!(await keyFile.exists())) {
      await keyFile.create(recursive: true);
      try {
        keyFile.writeAsBytes((await _getPublicKey())!);
      } catch (e) {
        await keyFile.delete();
      }
    }
  }

  String _makeExpiryString() {
    return _calculateExpiryDate(_formData) != null
        ? '${_dateFormat.format(_calculateExpiryDate(_formData)!)} ${(/*_formData.isTested*/ true && _calculateExpiryDate(_formData) != null) ? _timeFormat.format(_calculateExpiryDate(_formData)!) : ''}'
        : 'Bitte zuerst die Nachweisart wählen';
  }

  Future<String> _onEncrypt() async {
    final basePath = (await getApplicationDocumentsDirectory()).path;

    RSAPublicKey publicKey;
    try {
      publicKey = await parseKeyFromFile<RSAPublicKey>('$basePath/public_key.pem');
    } catch (e) {
      final keyFile = File('$basePath/public_key.pem');
      if (keyFile.existsSync()) {
        await File('$basePath/public_key.pem').delete();
      }
      await keyFile.writeAsBytes((await _getPublicKey())!);
      publicKey = await parseKeyFromFile<RSAPublicKey>('$basePath/public_key.pem');
    }

    final encrypter = Encrypter(RSA(publicKey: publicKey, encoding: RSAEncoding.OAEP));

    final expiry = _calculateExpiryDate(_formData);

    if (expiry != null) {
      final encrypted =
          encrypter.encrypt(_calculateExpiryDate(_formData)!.toUtc().toIso8601String());
      return base64.encode(encrypted.bytes);
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('Endzeitpunkt konnte nicht errechnet werden')));
      return '';
    }
  }

  Future<void> _onShowCode() async {
    final encrypted = await _onEncrypt();

    final signed = await _userApi!.signTicket(encrypted);

Leon Tappe's avatar
Leon Tappe committed
271
272
    if (signed == null) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
Leon Tappe's avatar
Leon Tappe committed
273
274
        content: Text(
            'Code konnte nicht signiert werden. Bitte prüfe die Eingaben auf Korrekt- und Sinnhaftigkeit.'),
Leon Tappe's avatar
Leon Tappe committed
275
276
277
278
        duration: Duration(seconds: 5),
      ));
      return;
    }
279
280
281
282
283
284
285
286
287
288
289
290
291
292

    await showDialog(
      context: context,
      builder: (BuildContext context) => Dialog(
        insetPadding: const EdgeInsets.all(8.0),
        child: QrImage(
          data: signed.combinedString,
          version: QrVersions.auto,
          size: MediaQuery.of(context).size.width - 32.0,
        ),
      ),
    );
  }
}