From 4d12d243b3ba6a4bedfe4cce4304ab176f874a05 Mon Sep 17 00:00:00 2001
From: LittleSheep <littlesheep.code@hotmail.com>
Date: Sat, 9 Nov 2024 18:28:45 +0800
Subject: [PATCH] :sparkles: Login & register

---
 assets/translations/en-US.json               |   42 +-
 assets/translations/zh-CN.json               |   42 +-
 ios/Podfile.lock                             |    6 +
 lib/main.dart                                |    6 +-
 lib/providers/sn_network.dart                |   88 ++
 lib/providers/userinfo.dart                  |   43 +-
 lib/router.dart                              |   22 +-
 lib/screens/account.dart                     |   25 +-
 lib/screens/auth/login.dart                  |  556 ++++++++
 lib/screens/auth/register.dart               |  161 +++
 lib/types/account.dart                       |   69 +
 lib/types/account.freezed.dart               | 1266 ++++++++++++++++++
 lib/types/account.g.dart                     |  131 ++
 lib/types/auth.dart                          |   57 +
 lib/types/auth.freezed.dart                  | 1002 ++++++++++++++
 lib/types/auth.g.dart                        |   98 ++
 lib/widgets/dialog.dart                      |  151 +++
 lib/widgets/navigation/app_scaffold.dart     |   34 +-
 linux/flutter/generated_plugin_registrant.cc |    4 +
 linux/flutter/generated_plugins.cmake        |    1 +
 pubspec.lock                                 |   12 +-
 pubspec.yaml                                 |    2 +
 22 files changed, 3798 insertions(+), 20 deletions(-)
 create mode 100644 lib/screens/auth/login.dart
 create mode 100644 lib/screens/auth/register.dart
 create mode 100644 lib/types/account.dart
 create mode 100644 lib/types/account.freezed.dart
 create mode 100644 lib/types/account.g.dart
 create mode 100644 lib/types/auth.dart
 create mode 100644 lib/types/auth.freezed.dart
 create mode 100644 lib/types/auth.g.dart
 create mode 100644 lib/widgets/dialog.dart

diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json
index eff1db6..8381f11 100644
--- a/assets/translations/en-US.json
+++ b/assets/translations/en-US.json
@@ -1,5 +1,39 @@
 {
-    "screenHome": "Home",
-    "screenExplore": "Explore",
-    "screenAccount": "Account"
-}
\ No newline at end of file
+  "screen": "Screen",
+  "screenHome": "Home",
+  "screenExplore": "Explore",
+  "screenAccount": "Account",
+  "screenAuthLogin": "Login",
+  "screenAuthLoginSubtitle": "Login to Solar Network using Solarpass",
+  "screenAuthLoginGreeting": "Welcome back",
+  "screenAuthRegister": "Create an account",
+  "screenAuthRegisterSubtitle": "Create a Solarpass account",
+  "dialogOkay": "Okay",
+  "dialogCancel": "Cancel",
+  "dialogConfirm": "Confirm",
+  "dialogDismiss": "Dismiss",
+  "dialogError": "Something went wrong",
+  "errorRequestBad": "Bad request, please check your input.",
+  "errorRequestUnauthorized": "Unauthorized request, please login or try re-login.",
+  "errorRequestForbidden": "Forbidden request, you have not enough permission to do that.",
+  "errorRequestNotFound": "The resource that you looking for is not found.",
+  "errorRequestConnection": "Network connection error, please check your network or the service status.",
+  "errorRequestUnknown": "Unknown request error, maybe you want to take screenshot and report it to us.",
+  "prev": "Next",
+  "next": "Previous",
+  "fieldUsername": "Username",
+  "fieldNickname": "Nickname",
+  "fieldEmail": "Email address",
+  "fieldPassword": "Password",
+  "fieldUsernameLookupHint": "You can use username, phone number or email to login",
+  "forgotPassword": "Forgot password",
+  "loginPickFactor": "Pick a factor",
+  "loginMultiFactor": {
+    "one": "{} step left",
+    "other": "{} steps left"
+  },
+  "loginEnterPassword": "Enter the code",
+  "loginSuccess": "Logged in as {}",
+  "authFactorPassword": "Password",
+  "authFactorEmail": "Email verification code"
+}
diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json
index b6d3e0a..d2da117 100644
--- a/assets/translations/zh-CN.json
+++ b/assets/translations/zh-CN.json
@@ -1,5 +1,39 @@
 {
-    "screenHome": "首页",
-    "screenExplore": "探索" ,
-    "screenAccount": "您"
-}
\ No newline at end of file
+  "screen": "页面",
+  "screenHome": "首页",
+  "screenExplore": "探索",
+  "screenAccount": "您",
+  "screenAuthLogin": "登陆",
+  "screenAuthLoginSubtitle": "使用 Solarpass 登陆 Solar Network",
+  "screenAuthLoginGreeting": "欢迎回来",
+  "screenAuthRegister": "创建账号",
+  "screenAuthRegisterSubtitle": "创建一个 Solarpass 账号",
+  "dialogOkay": "好的",
+  "dialogCancel": "取消",
+  "dialogConfirm": "确认",
+  "dialogDismiss": "忽略",
+  "dialogError": "出了点问题",
+  "errorRequestBad": "服务器拒绝了您的请求,请检查您的输入。",
+  "errorRequestUnauthorized": "未授权的请求,请登录或者尝试重新登陆。",
+  "errorRequestForbidden": "被禁止的请求,您没有足够的权限去做那件事。",
+  "errorRequestNotFound": "您正查找的资源无法被找到。",
+  "errorRequestConnection": "网络连接错误,请检查您的网络状态或者检查我们的服务状态。",
+  "errorRequestUnknown": "位置请求错误,您可能想将此对话框截图并发送给我们。",
+  "prev": "上一步",
+  "next": "下一步",
+  "fieldUsername": "用户名",
+  "fieldNickname": "显示名",
+  "fieldEmail": "电子邮箱地址",
+  "fieldPassword": "密码",
+  "fieldUsernameLookupHint": "支持用户名、电话号码或邮箱地址",
+  "forgotPassword": "忘记密码",
+  "loginPickFactor": "选择方式验证",
+  "loginMultiFactor": {
+    "one": "{} 步验证",
+    "other": "{} 步验证"
+  },
+  "loginEnterPassword": "验证代码",
+  "loginSuccess": "登录为 {}",
+  "authFactorPassword": "密码",
+  "authFactorEmail": "电邮一次性验证码"
+}
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 252a418..0e88c5a 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -7,6 +7,8 @@ PODS:
   - Flutter (1.0.0)
   - flutter_native_splash (0.0.1):
     - Flutter
+  - flutter_secure_storage (3.3.1):
+    - Flutter
   - path_provider_foundation (0.0.1):
     - Flutter
     - FlutterMacOS
@@ -24,6 +26,7 @@ DEPENDENCIES:
   - cupertino_http (from `.symlinks/plugins/cupertino_http/ios`)
   - Flutter (from `Flutter`)
   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
+  - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
   - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
   - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
@@ -38,6 +41,8 @@ EXTERNAL SOURCES:
     :path: Flutter
   flutter_native_splash:
     :path: ".symlinks/plugins/flutter_native_splash/ios"
+  flutter_secure_storage:
+    :path: ".symlinks/plugins/flutter_secure_storage/ios"
   path_provider_foundation:
     :path: ".symlinks/plugins/path_provider_foundation/darwin"
   shared_preferences_foundation:
@@ -52,6 +57,7 @@ SPEC CHECKSUMS:
   cupertino_http: 1a3a0f163c1b26e7f1a293b33d476e0fde7a64ec
   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
   flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
+  flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
   path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
   shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
   sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
diff --git a/lib/main.dart b/lib/main.dart
index 02cf71d..637b18a 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -33,11 +33,15 @@ class SolianApp extends StatelessWidget {
           providers: [
             Provider(create: (_) => SnNetworkProvider()),
             Provider(create: (ctx) => SnAttachmentProvider(ctx)),
-            ChangeNotifierProvider(create: (_) => UserProvider()),
+            ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
             ChangeNotifierProvider(create: (_) => ThemeProvider()),
           ],
           child: Builder(builder: (context) {
+            // Initialize some providers
+            context.read<UserProvider>();
+
             final th = context.watch<ThemeProvider>();
+
             return MaterialApp.router(
               theme: th.theme.light,
               darkTheme: th.theme.dark,
diff --git a/lib/providers/sn_network.dart b/lib/providers/sn_network.dart
index b7f042a..8eed0e1 100644
--- a/lib/providers/sn_network.dart
+++ b/lib/providers/sn_network.dart
@@ -1,15 +1,23 @@
+import 'dart:convert';
+import 'dart:developer';
 import 'dart:io';
 
 import 'package:dio/dio.dart';
 import 'package:dio_smart_retry/dio_smart_retry.dart';
 import 'package:flutter/foundation.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
 import 'package:native_dio_adapter/native_dio_adapter.dart';
 
 const kUseLocalNetwork = true;
 
+const kAtkStoreKey = 'nex_user_atk';
+const kRtkStoreKey = 'nex_user_rtk';
+
 class SnNetworkProvider {
   late final Dio client;
 
+  late final FlutterSecureStorage _storage = FlutterSecureStorage();
+
   SnNetworkProvider() {
     client = Dio();
 
@@ -27,6 +35,56 @@ class SnNetworkProvider {
       ],
     ));
 
+    client.interceptors.add(
+      InterceptorsWrapper(
+        onRequest: (
+          RequestOptions options,
+          RequestInterceptorHandler handler,
+        ) async {
+          try {
+            var atk = await _storage.read(key: kAtkStoreKey);
+            if (atk != null) {
+              final atkParts = atk.split('.');
+              if (atkParts.length != 3) {
+                throw Exception('invalid format of access token');
+              }
+
+              var rawPayload =
+                  atkParts[1].replaceAll('-', '+').replaceAll('_', '/');
+              switch (rawPayload.length % 4) {
+                case 0:
+                  break;
+                case 2:
+                  rawPayload += '==';
+                  break;
+                case 3:
+                  rawPayload += '=';
+                  break;
+                default:
+                  throw Exception('illegal format of access token payload');
+              }
+
+              final b64 = utf8.fuse(base64Url);
+              final payload = b64.decode(rawPayload);
+              final exp = jsonDecode(payload)['exp'];
+              if (exp >= DateTime.now().millisecondsSinceEpoch) {
+                log('Access token need refresh, doing it at ${DateTime.now()}');
+                atk = await refreshToken();
+              }
+
+              if (atk != null) {
+                options.headers['Authorization'] = 'Bearer $atk';
+              } else {
+                log('Access token refresh failed...');
+              }
+            }
+          } finally {
+            handler.next(options);
+          }
+        },
+      ),
+    );
+
     if (!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) {
       // Switch to native implementation if possible
       client.httpClientAdapter = NativeAdapter();
@@ -37,4 +95,34 @@ class SnNetworkProvider {
     if (ky.startsWith("http://")) return ky;
     return '${client.options.baseUrl}/cgi/uc/attachments/$ky';
   }
+
+  Future<void> setTokenPair(String atk, String rtk) async {
+    await Future.wait([
+      _storage.write(key: kAtkStoreKey, value: atk),
+      _storage.write(key: kRtkStoreKey, value: rtk),
+    ]);
+  }
+
+  Future<void> clearTokenPair() async {
+    await Future.wait([
+      _storage.delete(key: kAtkStoreKey),
+      _storage.delete(key: kRtkStoreKey),
+    ]);
+  }
+
+  Future<String?> refreshToken() async {
+    final rtk = await _storage.read(key: kRtkStoreKey);
+    if (rtk == null) return null;
+
+    final resp = await client.post('/cgi/id/auth/token', data: {
+      'grant_type': 'refresh_token',
+      'refresh_token': rtk,
+    });
+
+    final atk = resp.data['access_token'];
+    final nRtk = resp.data['refresh_token'];
+    await setTokenPair(atk, nRtk);
+
+    return atk;
+  }
 }
diff --git a/lib/providers/userinfo.dart b/lib/providers/userinfo.dart
index 1fbc24b..4b37797 100644
--- a/lib/providers/userinfo.dart
+++ b/lib/providers/userinfo.dart
@@ -1,3 +1,42 @@
-import 'package:flutter/foundation.dart';
+import 'dart:developer';
 
-class UserProvider extends ChangeNotifier {}
+import 'package:flutter/material.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:provider/provider.dart';
+import 'package:surface/providers/sn_network.dart';
+import 'package:surface/types/account.dart';
+
+class UserProvider extends ChangeNotifier {
+  bool isAuthorized = false;
+  SnAccount? user;
+
+  late final SnNetworkProvider sn;
+
+  late final FlutterSecureStorage _storage = FlutterSecureStorage();
+
+  UserProvider(BuildContext context) {
+    sn = context.read<SnNetworkProvider>();
+
+    _storage.read(key: kAtkStoreKey).then((value) {
+      isAuthorized = value != null;
+      notifyListeners();
+      refreshUser().then((value) {
+        if (value != null) {
+          log('Logged in as @${value.name}');
+        }
+      });
+    });
+  }
+
+  Future<SnAccount?> refreshUser() async {
+    if (!isAuthorized) return null;
+
+    final resp = await sn.client.get('/cgi/id/users/me');
+    final out = SnAccount.fromJson(resp.data);
+
+    user = out;
+    notifyListeners();
+
+    return out;
+  }
+}
diff --git a/lib/router.dart b/lib/router.dart
index 8fa1645..fc956ae 100644
--- a/lib/router.dart
+++ b/lib/router.dart
@@ -1,5 +1,7 @@
 import 'package:go_router/go_router.dart';
 import 'package:surface/screens/account.dart';
+import 'package:surface/screens/auth/login.dart';
+import 'package:surface/screens/auth/register.dart';
 import 'package:surface/screens/explore.dart';
 import 'package:surface/screens/home.dart';
 import 'package:surface/widgets/navigation/app_scaffold.dart';
@@ -28,6 +30,24 @@ final appRouter = GoRouter(
           builder: (context, state) => const AccountScreen(),
         ),
       ],
-    )
+    ),
+    ShellRoute(
+      builder: (context, state, child) => AppScaffold(
+        body: child,
+        autoImplyAppBar: true,
+      ),
+      routes: [
+        GoRoute(
+          path: '/auth/login',
+          name: 'authLogin',
+          builder: (context, state) => const LoginScreen(),
+        ),
+        GoRoute(
+          path: '/auth.register',
+          name: 'authRegister',
+          builder: (context, state) => const RegisterScreen(),
+        ),
+      ],
+    ),
   ],
 );
diff --git a/lib/screens/account.dart b/lib/screens/account.dart
index 8f970a0..b0ae87a 100644
--- a/lib/screens/account.dart
+++ b/lib/screens/account.dart
@@ -1,5 +1,6 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
 import 'package:surface/widgets/navigation/app_scaffold.dart';
 
 class AccountScreen extends StatefulWidget {
@@ -14,7 +15,29 @@ class _AccountScreenState extends State<AccountScreen> {
   Widget build(BuildContext context) {
     return AppScaffold(
       appBar: AppBar(
-        title: Text("screenHome").tr(),
+        title: Text("screenAccount").tr(),
+      ),
+      body: ListView(
+        children: [
+          ListTile(
+            title: Text('screenAuthLogin').tr(),
+            subtitle: Text('screenAuthLoginSubtitle').tr(),
+            contentPadding: const EdgeInsets.symmetric(horizontal: 24),
+            trailing: const Icon(Icons.chevron_right),
+            onTap: () {
+              GoRouter.of(context).pushNamed('authLogin');
+            },
+          ),
+          ListTile(
+            title: Text('screenAuthRegister').tr(),
+            subtitle: Text('screenAuthRegisterSubtitle').tr(),
+            contentPadding: const EdgeInsets.symmetric(horizontal: 24),
+            trailing: const Icon(Icons.chevron_right),
+            onTap: () {
+              GoRouter.of(context).pushNamed('authRegister');
+            },
+          )
+        ],
       ),
     );
   }
diff --git a/lib/screens/auth/login.dart b/lib/screens/auth/login.dart
new file mode 100644
index 0000000..b34f73a
--- /dev/null
+++ b/lib/screens/auth/login.dart
@@ -0,0 +1,556 @@
+import 'package:animations/animations.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:gap/gap.dart';
+import 'package:material_symbols_icons/symbols.dart';
+import 'package:provider/provider.dart';
+import 'package:styled_widget/styled_widget.dart';
+import 'package:surface/providers/sn_network.dart';
+import 'package:surface/providers/userinfo.dart';
+import 'package:surface/types/auth.dart';
+import 'package:surface/widgets/dialog.dart';
+import 'package:url_launcher/url_launcher_string.dart';
+
+final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = {
+  0: ('authFactorPassword'.tr(), Icons.password, false),
+  1: ('authFactorEmail'.tr(), Icons.email, true),
+};
+
+class LoginScreen extends StatefulWidget {
+  const LoginScreen({super.key});
+
+  @override
+  State<LoginScreen> createState() => _LoginScreenState();
+}
+
+class _LoginScreenState extends State<LoginScreen> {
+  SnAuthTicket? _currentTicket;
+
+  List<SnAuthFactor>? _factors;
+  SnAuthFactor? _factorPicked;
+
+  int _period = 0;
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      constraints: const BoxConstraints(maxWidth: 280),
+      child: Theme(
+        data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
+        child: SingleChildScrollView(
+          child: PageTransitionSwitcher(
+            transitionBuilder: (
+              Widget child,
+              Animation<double> primaryAnimation,
+              Animation<double> secondaryAnimation,
+            ) {
+              return SharedAxisTransition(
+                animation: primaryAnimation,
+                secondaryAnimation: secondaryAnimation,
+                transitionType: SharedAxisTransitionType.horizontal,
+                child: child,
+              );
+            },
+            child: switch (_period % 3) {
+              1 => _LoginPickerScreen(
+                  key: const ValueKey(1),
+                  ticket: _currentTicket,
+                  factors: _factors,
+                  onTicket: (p0) => setState(() {
+                    _currentTicket = p0;
+                  }),
+                  onPickFactor: (p0) => setState(() {
+                    _factorPicked = p0;
+                  }),
+                  onNext: () => setState(() {
+                    _period++;
+                  }),
+                ),
+              2 => _LoginCheckScreen(
+                  key: const ValueKey(2),
+                  ticket: _currentTicket,
+                  factor: _factorPicked,
+                  onTicket: (p0) => setState(() {
+                    _currentTicket = p0;
+                  }),
+                  onNext: (p0) => setState(() {
+                    _period = 1;
+                  }),
+                ),
+              _ => _LoginLookupScreen(
+                  key: const ValueKey(0),
+                  ticket: _currentTicket,
+                  onTicket: (p0) => setState(() {
+                    _currentTicket = p0;
+                  }),
+                  onFactor: (p0) => setState(() {
+                    _factors = p0;
+                  }),
+                  onNext: () => setState(() {
+                    _period++;
+                  }),
+                ),
+            },
+          ).padding(all: 24),
+        ).center(),
+      ),
+    );
+  }
+}
+
+class _LoginCheckScreen extends StatefulWidget {
+  final SnAuthTicket? ticket;
+  final SnAuthFactor? factor;
+  final Function(SnAuthTicket?) onTicket;
+  final Function onNext;
+  const _LoginCheckScreen({
+    super.key,
+    required this.ticket,
+    required this.factor,
+    required this.onTicket,
+    required this.onNext,
+  });
+
+  @override
+  State<_LoginCheckScreen> createState() => _LoginCheckScreenState();
+}
+
+class _LoginCheckScreenState extends State<_LoginCheckScreen> {
+  bool _isBusy = false;
+
+  final _passwordController = TextEditingController();
+
+  void _performCheckTicket() async {
+    final password = _passwordController.value.text;
+    if (password.isEmpty) return;
+
+    final sn = context.read<SnNetworkProvider>();
+
+    setState(() => _isBusy = true);
+
+    try {
+      // Check ticket
+      final resp = await sn.client.patch('/cgi/id/auth', data: {
+        'ticket_id': widget.ticket!.id,
+        'factor_id': widget.factor!.id,
+        'code': password,
+      });
+
+      final result = SnAuthResult.fromJson(resp.data);
+      widget.onTicket(result.ticket);
+
+      if (!result.isFinished) {
+        widget.onNext();
+        return;
+      }
+
+      // Finish sign in if possible
+      final tokenResp = await sn.client.post('/cgi/id/auth/token', data: {
+        'grant_type': 'grant_token',
+        'code': result.ticket!.grantToken,
+      });
+      final atk = tokenResp.data['access_token'];
+      final rtk = tokenResp.data['refresh_token'];
+      await sn.setTokenPair(atk, rtk);
+      if (!mounted) return;
+      final user = context.read<UserProvider>();
+      final userinfo = await user.refreshUser();
+      context.showSnackbar('loginSuccess'.tr(args: [
+        '@${userinfo!.name} (${userinfo.nick})',
+      ]));
+
+      Navigator.pop(context);
+    } catch (err) {
+      context.showErrorDialog(err);
+      return;
+    } finally {
+      setState(() => _isBusy = false);
+    }
+  }
+
+  @override
+  void dispose() {
+    _passwordController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Align(
+          alignment: Alignment.centerLeft,
+          child: CircleAvatar(
+            radius: 26,
+            child: const Icon(
+              Symbols.password,
+              size: 28,
+            ),
+          ).padding(bottom: 8),
+        ),
+        Text(
+          'loginEnterPassword'.tr(),
+          style: const TextStyle(
+            fontSize: 28,
+            fontWeight: FontWeight.w900,
+          ),
+        ).padding(left: 4, bottom: 16),
+        TextField(
+          autocorrect: false,
+          enableSuggestions: false,
+          controller: _passwordController,
+          obscureText: true,
+          autofillHints: [
+            (_factorLabelMap[widget.factor!.type]?.$3 ?? true)
+                ? AutofillHints.password
+                : AutofillHints.oneTimeCode
+          ],
+          decoration: InputDecoration(
+            isDense: true,
+            border: const UnderlineInputBorder(),
+            labelText: 'fieldPassword'.tr(),
+          ),
+          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
+          onSubmitted: _isBusy ? null : (_) => _performCheckTicket(),
+        ).padding(horizontal: 7),
+        const Gap(12),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.end,
+          children: [
+            TextButton(
+              onPressed: _isBusy ? null : () => _performCheckTicket(),
+              child: Row(
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  Text('next').tr(),
+                  const Icon(Icons.chevron_right),
+                ],
+              ),
+            ),
+          ],
+        ),
+      ],
+    );
+  }
+}
+
+class _LoginPickerScreen extends StatefulWidget {
+  final SnAuthTicket? ticket;
+  final List<SnAuthFactor>? factors;
+  final Function(SnAuthTicket?) onTicket;
+  final Function(SnAuthFactor) onPickFactor;
+  final Function onNext;
+  const _LoginPickerScreen({
+    super.key,
+    required this.ticket,
+    required this.factors,
+    required this.onTicket,
+    required this.onPickFactor,
+    required this.onNext,
+  });
+
+  @override
+  State<_LoginPickerScreen> createState() => _LoginPickerScreenState();
+}
+
+class _LoginPickerScreenState extends State<_LoginPickerScreen> {
+  bool _isBusy = false;
+  int? _factorPicked;
+
+  Color get _unFocusColor =>
+      Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
+
+  void _performGetFactorCode() async {
+    if (_factorPicked == null) return;
+
+    final sn = context.read<SnNetworkProvider>();
+
+    setState(() => _isBusy = true);
+
+    try {
+      // Request one-time-password code
+      sn.client.post('/cgi/id/auth/factors/$_factorPicked');
+      widget.onPickFactor(
+        widget.factors!.where((x) => x.id == _factorPicked).first,
+      );
+      widget.onNext();
+    } catch (err) {
+      context.showErrorDialog(err);
+      return;
+    } finally {
+      setState(() => _isBusy = false);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      key: const ValueKey<int>(1),
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Align(
+          alignment: Alignment.centerLeft,
+          child: CircleAvatar(
+            radius: 26,
+            child: const Icon(
+              Symbols.security,
+              size: 28,
+            ),
+          ).padding(bottom: 8),
+        ),
+        Text(
+          'loginPickFactor',
+          style: const TextStyle(
+            fontSize: 28,
+            fontWeight: FontWeight.w900,
+          ),
+        ).tr().padding(left: 4),
+        const Gap(8),
+        Card(
+          margin: const EdgeInsets.symmetric(vertical: 4),
+          child: Column(
+            children: widget.factors
+                    ?.map(
+                      (x) => CheckboxListTile(
+                        shape: const RoundedRectangleBorder(
+                          borderRadius: BorderRadius.all(
+                            Radius.circular(8),
+                          ),
+                        ),
+                        secondary: Icon(
+                          _factorLabelMap[x.type]?.$2 ?? Icons.question_mark,
+                        ),
+                        title: Text(
+                          _factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(),
+                        ),
+                        enabled: !widget.ticket!.factorTrail.contains(x.id),
+                        value: _factorPicked == x.id,
+                        onChanged: (value) {
+                          if (value == true) {
+                            setState(() => _factorPicked = x.id);
+                          }
+                        },
+                      ),
+                    )
+                    .toList() ??
+                List.empty(),
+          ),
+        ),
+        const Gap(8),
+        Text(
+          'loginMultiFactor'.plural(
+            widget.ticket!.stepRemain,
+          ),
+          style: TextStyle(color: _unFocusColor, fontSize: 13),
+        ).padding(horizontal: 16),
+        const Gap(12),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.end,
+          children: [
+            TextButton(
+              onPressed: _isBusy ? null : () => _performGetFactorCode(),
+              child: Row(
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  Text('next'.tr()),
+                  const Icon(Icons.chevron_right),
+                ],
+              ),
+            ),
+          ],
+        ),
+      ],
+    );
+  }
+}
+
+class _LoginLookupScreen extends StatefulWidget {
+  final SnAuthTicket? ticket;
+  final Function(SnAuthTicket?) onTicket;
+  final Function(List<SnAuthFactor>?) onFactor;
+  final Function onNext;
+  const _LoginLookupScreen({
+    super.key,
+    required this.ticket,
+    required this.onTicket,
+    required this.onFactor,
+    required this.onNext,
+  });
+
+  @override
+  State<_LoginLookupScreen> createState() => _LoginLookupScreenState();
+}
+
+class _LoginLookupScreenState extends State<_LoginLookupScreen> {
+  final _usernameController = TextEditingController();
+
+  bool _isBusy = false;
+
+  void _requestResetPassword() async {
+    final username = _usernameController.value.text;
+    if (username.isEmpty) {
+      context.showErrorDialog('signinResetPasswordHint'.tr());
+      return;
+    }
+
+    setState(() => _isBusy = true);
+
+    try {
+      final sn = context.read<SnNetworkProvider>();
+      final lookupResp =
+          await sn.client.get('/cgi/id/users/lookup?probe=$username');
+      await sn.client.post('/cgi/id/users/me/password-reset', data: {
+        'user_id': lookupResp.data['id'],
+      });
+      context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
+    } catch (err) {
+      context.showErrorDialog(err);
+    } finally {
+      setState(() => _isBusy = false);
+    }
+  }
+
+  void _performNewTicket() async {
+    final username = _usernameController.value.text;
+    if (username.isEmpty) return;
+
+    final sn = context.read<SnNetworkProvider>();
+
+    setState(() => _isBusy = true);
+
+    try {
+      // Create ticket
+      final resp = await sn.client.post('/cgi/id/auth', data: {
+        'username': username,
+      });
+      final result = SnAuthResult.fromJson(resp.data);
+      widget.onTicket(result.ticket);
+
+      // Pull factors
+      final factorResp =
+          await sn.client.get('/cgi/id/auth/factors', queryParameters: {
+        'ticketId': result.ticket!.id.toString(),
+      });
+      widget.onFactor(
+        List<SnAuthFactor>.from(
+          factorResp.data.map((ele) => SnAuthFactor.fromJson(ele)),
+        ),
+      );
+
+      widget.onNext();
+    } catch (err) {
+      context.showErrorDialog(err);
+      return;
+    } finally {
+      setState(() => _isBusy = false);
+    }
+  }
+
+  @override
+  void dispose() {
+    _usernameController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Align(
+          alignment: Alignment.centerLeft,
+          child: CircleAvatar(
+            radius: 26,
+            child: const Icon(
+              Symbols.login,
+              size: 28,
+            ),
+          ).padding(bottom: 8),
+        ),
+        Text(
+          'screenAuthLoginGreeting',
+          style: const TextStyle(
+            fontSize: 28,
+            fontWeight: FontWeight.w900,
+          ),
+        ).tr().padding(left: 4, bottom: 16),
+        TextField(
+          autocorrect: false,
+          enableSuggestions: false,
+          controller: _usernameController,
+          autofillHints: const [AutofillHints.username],
+          decoration: InputDecoration(
+            isDense: true,
+            border: const UnderlineInputBorder(),
+            labelText: 'fieldUsername'.tr(),
+            helperText: 'fieldUsernameLookupHint'.tr(),
+          ),
+          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
+          onSubmitted: _isBusy ? null : (_) => _performNewTicket(),
+        ).padding(horizontal: 7),
+        const Gap(12),
+        Row(
+          mainAxisAlignment: MainAxisAlignment.spaceBetween,
+          children: [
+            TextButton(
+              onPressed: _isBusy ? null : () => _requestResetPassword(),
+              style: TextButton.styleFrom(foregroundColor: Colors.grey),
+              child: Text('forgotPassword'.tr()),
+            ),
+            TextButton(
+              onPressed: _isBusy ? null : () => _performNewTicket(),
+              child: Row(
+                mainAxisSize: MainAxisSize.min,
+                children: [
+                  Text('next').tr(),
+                  const Icon(Icons.chevron_right),
+                ],
+              ),
+            ),
+          ],
+        ),
+        const Gap(12),
+        Align(
+          alignment: Alignment.centerRight,
+          child: StyledWidget(
+            Container(
+              constraints: const BoxConstraints(maxWidth: 290),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.end,
+                children: [
+                  Text(
+                    'termAcceptNextWithAgree'.tr(),
+                    textAlign: TextAlign.end,
+                    style: Theme.of(context).textTheme.bodySmall!.copyWith(
+                          color: Theme.of(context)
+                              .colorScheme
+                              .onSurface
+                              .withAlpha((255 * 0.75).round()),
+                        ),
+                  ),
+                  Material(
+                    color: Colors.transparent,
+                    child: InkWell(
+                      child: Row(
+                        mainAxisSize: MainAxisSize.min,
+                        children: [
+                          Text('termAcceptLink'.tr()),
+                          const Gap(4),
+                          const Icon(Icons.launch, size: 14),
+                        ],
+                      ),
+                      onTap: () {
+                        launchUrlString('https://solsynth.dev/terms');
+                      },
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ).padding(horizontal: 16),
+        ),
+      ],
+    );
+  }
+}
diff --git a/lib/screens/auth/register.dart b/lib/screens/auth/register.dart
new file mode 100644
index 0000000..bc70375
--- /dev/null
+++ b/lib/screens/auth/register.dart
@@ -0,0 +1,161 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:gap/gap.dart';
+import 'package:material_symbols_icons/symbols.dart';
+import 'package:provider/provider.dart';
+import 'package:styled_widget/styled_widget.dart';
+import 'package:surface/providers/sn_network.dart';
+import 'package:surface/widgets/dialog.dart';
+
+class RegisterScreen extends StatefulWidget {
+  const RegisterScreen({super.key});
+
+  @override
+  State<RegisterScreen> createState() => _RegisterScreenState();
+}
+
+class _RegisterScreenState extends State<RegisterScreen> {
+  final _emailController = TextEditingController();
+  final _usernameController = TextEditingController();
+  final _nicknameController = TextEditingController();
+  final _passwordController = TextEditingController();
+
+  void _performAction(BuildContext context) async {
+    final email = _emailController.value.text;
+    final username = _usernameController.value.text;
+    final nickname = _nicknameController.value.text;
+    final password = _passwordController.value.text;
+    if (email.isEmpty ||
+        username.isEmpty ||
+        nickname.isEmpty ||
+        password.isEmpty) {
+      return;
+    }
+
+    try {
+      final sn = context.read<SnNetworkProvider>();
+      await sn.client.post('/cgi/id/users', data: {
+        'name': username,
+        'nick': nickname,
+        'email': email,
+        'password': password,
+      });
+
+      if (!mounted) return;
+
+      // TODO make celebration here
+      // ignore: use_build_context_synchronously
+      Navigator.pop(context);
+    } catch (err) {
+      context.showErrorDialog(err);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      constraints: const BoxConstraints(maxWidth: 280),
+      child: StyledWidget(
+        SingleChildScrollView(
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Align(
+                alignment: Alignment.centerLeft,
+                child: CircleAvatar(
+                  radius: 26,
+                  child: const Icon(
+                    Symbols.person_add,
+                    size: 28,
+                  ),
+                ).padding(bottom: 8),
+              ),
+              Text(
+                'screenAuthRegister',
+                style: const TextStyle(
+                  fontSize: 28,
+                  fontWeight: FontWeight.w900,
+                ),
+              ).tr().padding(left: 4, bottom: 16),
+              Column(
+                children: [
+                  TextField(
+                    autocorrect: false,
+                    enableSuggestions: false,
+                    controller: _usernameController,
+                    autofillHints: const [AutofillHints.username],
+                    decoration: InputDecoration(
+                      isDense: true,
+                      border: const UnderlineInputBorder(),
+                      labelText: 'fieldUsername'.tr(),
+                    ),
+                    onTapOutside: (_) =>
+                        FocusManager.instance.primaryFocus?.unfocus(),
+                  ),
+                  const Gap(12),
+                  TextField(
+                    autocorrect: false,
+                    enableSuggestions: false,
+                    controller: _nicknameController,
+                    autofillHints: const [AutofillHints.nickname],
+                    decoration: InputDecoration(
+                      isDense: true,
+                      border: const UnderlineInputBorder(),
+                      labelText: 'fieldNickname'.tr(),
+                    ),
+                    onTapOutside: (_) =>
+                        FocusManager.instance.primaryFocus?.unfocus(),
+                  ),
+                  const Gap(12),
+                  TextField(
+                    autocorrect: false,
+                    enableSuggestions: false,
+                    controller: _emailController,
+                    autofillHints: const [AutofillHints.email],
+                    decoration: InputDecoration(
+                      isDense: true,
+                      border: const UnderlineInputBorder(),
+                      labelText: 'fieldEmail'.tr(),
+                    ),
+                    onTapOutside: (_) =>
+                        FocusManager.instance.primaryFocus?.unfocus(),
+                  ),
+                  const Gap(12),
+                  TextField(
+                    obscureText: true,
+                    autocorrect: false,
+                    enableSuggestions: false,
+                    autofillHints: const [AutofillHints.password],
+                    controller: _passwordController,
+                    decoration: InputDecoration(
+                      isDense: true,
+                      border: const UnderlineInputBorder(),
+                      labelText: 'fieldPassword'.tr(),
+                    ),
+                    onTapOutside: (_) =>
+                        FocusManager.instance.primaryFocus?.unfocus(),
+                    onSubmitted: (_) => _performAction(context),
+                  ),
+                ],
+              ).padding(horizontal: 7),
+              const Gap(16),
+              Align(
+                alignment: Alignment.centerRight,
+                child: TextButton(
+                  onPressed: () => _performAction(context),
+                  child: Row(
+                    mainAxisSize: MainAxisSize.min,
+                    children: [
+                      Text('next').tr(),
+                      const Icon(Icons.chevron_right),
+                    ],
+                  ),
+                ),
+              )
+            ],
+          ),
+        ),
+      ).padding(all: 24).center(),
+    );
+  }
+}
diff --git a/lib/types/account.dart b/lib/types/account.dart
new file mode 100644
index 0000000..345c397
--- /dev/null
+++ b/lib/types/account.dart
@@ -0,0 +1,69 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'account.freezed.dart';
+part 'account.g.dart';
+
+@freezed
+class SnAccount with _$SnAccount {
+  const factory SnAccount({
+    required int id,
+    required int? affiliatedId,
+    required int? affiliatedTo,
+    required int? automatedBy,
+    required int? automatedId,
+    required String avatar,
+    required String banner,
+    required DateTime? confirmedAt,
+    required List<SnAccountContact> contacts,
+    required DateTime createdAt,
+    required DateTime? deletedAt,
+    required String description,
+    required String name,
+    required String nick,
+    required Map<String, dynamic> permNodes,
+    required SnAccountProfile? profile,
+    required DateTime? suspendedAt,
+    required DateTime updatedAt,
+  }) = _SnAccount;
+
+  factory SnAccount.fromJson(Map<String, Object?> json) =>
+      _$SnAccountFromJson(json);
+}
+
+@freezed
+class SnAccountContact with _$SnAccountContact {
+  const factory SnAccountContact({
+    required int accountId,
+    required String content,
+    required DateTime createdAt,
+    required DateTime? deletedAt,
+    required int id,
+    required bool isPrimary,
+    required bool isPublic,
+    required int type,
+    required DateTime updatedAt,
+    required DateTime? verifiedAt,
+  }) = _SnAccountContact;
+
+  factory SnAccountContact.fromJson(Map<String, Object?> json) =>
+      _$SnAccountContactFromJson(json);
+}
+
+@freezed
+class SnAccountProfile with _$SnAccountProfile {
+  const factory SnAccountProfile({
+    required int id,
+    required int accountId,
+    required DateTime? birthday,
+    required DateTime createdAt,
+    required DateTime? deletedAt,
+    required int experience,
+    required String firstName,
+    required String lastName,
+    required DateTime? lastSeenAt,
+    required DateTime updatedAt,
+  }) = _SnAccountProfile;
+
+  factory SnAccountProfile.fromJson(Map<String, Object?> json) =>
+      _$SnAccountProfileFromJson(json);
+}
diff --git a/lib/types/account.freezed.dart b/lib/types/account.freezed.dart
new file mode 100644
index 0000000..43f6bde
--- /dev/null
+++ b/lib/types/account.freezed.dart
@@ -0,0 +1,1266 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'account.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity<T>(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+    'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
+
+SnAccount _$SnAccountFromJson(Map<String, dynamic> json) {
+  return _SnAccount.fromJson(json);
+}
+
+/// @nodoc
+mixin _$SnAccount {
+  int get id => throw _privateConstructorUsedError;
+  int? get affiliatedId => throw _privateConstructorUsedError;
+  int? get affiliatedTo => throw _privateConstructorUsedError;
+  int? get automatedBy => throw _privateConstructorUsedError;
+  int? get automatedId => throw _privateConstructorUsedError;
+  String get avatar => throw _privateConstructorUsedError;
+  String get banner => throw _privateConstructorUsedError;
+  DateTime? get confirmedAt => throw _privateConstructorUsedError;
+  List<SnAccountContact> get contacts => throw _privateConstructorUsedError;
+  DateTime get createdAt => throw _privateConstructorUsedError;
+  DateTime? get deletedAt => throw _privateConstructorUsedError;
+  String get description => throw _privateConstructorUsedError;
+  String get name => throw _privateConstructorUsedError;
+  String get nick => throw _privateConstructorUsedError;
+  Map<String, dynamic> get permNodes => throw _privateConstructorUsedError;
+  SnAccountProfile? get profile => throw _privateConstructorUsedError;
+  DateTime? get suspendedAt => throw _privateConstructorUsedError;
+  DateTime get updatedAt => throw _privateConstructorUsedError;
+
+  /// Serializes this SnAccount to a JSON map.
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+
+  /// Create a copy of SnAccount
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  $SnAccountCopyWith<SnAccount> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $SnAccountCopyWith<$Res> {
+  factory $SnAccountCopyWith(SnAccount value, $Res Function(SnAccount) then) =
+      _$SnAccountCopyWithImpl<$Res, SnAccount>;
+  @useResult
+  $Res call(
+      {int id,
+      int? affiliatedId,
+      int? affiliatedTo,
+      int? automatedBy,
+      int? automatedId,
+      String avatar,
+      String banner,
+      DateTime? confirmedAt,
+      List<SnAccountContact> contacts,
+      DateTime createdAt,
+      DateTime? deletedAt,
+      String description,
+      String name,
+      String nick,
+      Map<String, dynamic> permNodes,
+      SnAccountProfile? profile,
+      DateTime? suspendedAt,
+      DateTime updatedAt});
+
+  $SnAccountProfileCopyWith<$Res>? get profile;
+}
+
+/// @nodoc
+class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount>
+    implements $SnAccountCopyWith<$Res> {
+  _$SnAccountCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  /// Create a copy of SnAccount
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? id = null,
+    Object? affiliatedId = freezed,
+    Object? affiliatedTo = freezed,
+    Object? automatedBy = freezed,
+    Object? automatedId = freezed,
+    Object? avatar = null,
+    Object? banner = null,
+    Object? confirmedAt = freezed,
+    Object? contacts = null,
+    Object? createdAt = null,
+    Object? deletedAt = freezed,
+    Object? description = null,
+    Object? name = null,
+    Object? nick = null,
+    Object? permNodes = null,
+    Object? profile = freezed,
+    Object? suspendedAt = freezed,
+    Object? updatedAt = null,
+  }) {
+    return _then(_value.copyWith(
+      id: null == id
+          ? _value.id
+          : id // ignore: cast_nullable_to_non_nullable
+              as int,
+      affiliatedId: freezed == affiliatedId
+          ? _value.affiliatedId
+          : affiliatedId // ignore: cast_nullable_to_non_nullable
+              as int?,
+      affiliatedTo: freezed == affiliatedTo
+          ? _value.affiliatedTo
+          : affiliatedTo // ignore: cast_nullable_to_non_nullable
+              as int?,
+      automatedBy: freezed == automatedBy
+          ? _value.automatedBy
+          : automatedBy // ignore: cast_nullable_to_non_nullable
+              as int?,
+      automatedId: freezed == automatedId
+          ? _value.automatedId
+          : automatedId // ignore: cast_nullable_to_non_nullable
+              as int?,
+      avatar: null == avatar
+          ? _value.avatar
+          : avatar // ignore: cast_nullable_to_non_nullable
+              as String,
+      banner: null == banner
+          ? _value.banner
+          : banner // ignore: cast_nullable_to_non_nullable
+              as String,
+      confirmedAt: freezed == confirmedAt
+          ? _value.confirmedAt
+          : confirmedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      contacts: null == contacts
+          ? _value.contacts
+          : contacts // ignore: cast_nullable_to_non_nullable
+              as List<SnAccountContact>,
+      createdAt: null == createdAt
+          ? _value.createdAt
+          : createdAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      deletedAt: freezed == deletedAt
+          ? _value.deletedAt
+          : deletedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      description: null == description
+          ? _value.description
+          : description // ignore: cast_nullable_to_non_nullable
+              as String,
+      name: null == name
+          ? _value.name
+          : name // ignore: cast_nullable_to_non_nullable
+              as String,
+      nick: null == nick
+          ? _value.nick
+          : nick // ignore: cast_nullable_to_non_nullable
+              as String,
+      permNodes: null == permNodes
+          ? _value.permNodes
+          : permNodes // ignore: cast_nullable_to_non_nullable
+              as Map<String, dynamic>,
+      profile: freezed == profile
+          ? _value.profile
+          : profile // ignore: cast_nullable_to_non_nullable
+              as SnAccountProfile?,
+      suspendedAt: freezed == suspendedAt
+          ? _value.suspendedAt
+          : suspendedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      updatedAt: null == updatedAt
+          ? _value.updatedAt
+          : updatedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+    ) as $Val);
+  }
+
+  /// Create a copy of SnAccount
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @pragma('vm:prefer-inline')
+  $SnAccountProfileCopyWith<$Res>? get profile {
+    if (_value.profile == null) {
+      return null;
+    }
+
+    return $SnAccountProfileCopyWith<$Res>(_value.profile!, (value) {
+      return _then(_value.copyWith(profile: value) as $Val);
+    });
+  }
+}
+
+/// @nodoc
+abstract class _$$SnAccountImplCopyWith<$Res>
+    implements $SnAccountCopyWith<$Res> {
+  factory _$$SnAccountImplCopyWith(
+          _$SnAccountImpl value, $Res Function(_$SnAccountImpl) then) =
+      __$$SnAccountImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call(
+      {int id,
+      int? affiliatedId,
+      int? affiliatedTo,
+      int? automatedBy,
+      int? automatedId,
+      String avatar,
+      String banner,
+      DateTime? confirmedAt,
+      List<SnAccountContact> contacts,
+      DateTime createdAt,
+      DateTime? deletedAt,
+      String description,
+      String name,
+      String nick,
+      Map<String, dynamic> permNodes,
+      SnAccountProfile? profile,
+      DateTime? suspendedAt,
+      DateTime updatedAt});
+
+  @override
+  $SnAccountProfileCopyWith<$Res>? get profile;
+}
+
+/// @nodoc
+class __$$SnAccountImplCopyWithImpl<$Res>
+    extends _$SnAccountCopyWithImpl<$Res, _$SnAccountImpl>
+    implements _$$SnAccountImplCopyWith<$Res> {
+  __$$SnAccountImplCopyWithImpl(
+      _$SnAccountImpl _value, $Res Function(_$SnAccountImpl) _then)
+      : super(_value, _then);
+
+  /// Create a copy of SnAccount
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? id = null,
+    Object? affiliatedId = freezed,
+    Object? affiliatedTo = freezed,
+    Object? automatedBy = freezed,
+    Object? automatedId = freezed,
+    Object? avatar = null,
+    Object? banner = null,
+    Object? confirmedAt = freezed,
+    Object? contacts = null,
+    Object? createdAt = null,
+    Object? deletedAt = freezed,
+    Object? description = null,
+    Object? name = null,
+    Object? nick = null,
+    Object? permNodes = null,
+    Object? profile = freezed,
+    Object? suspendedAt = freezed,
+    Object? updatedAt = null,
+  }) {
+    return _then(_$SnAccountImpl(
+      id: null == id
+          ? _value.id
+          : id // ignore: cast_nullable_to_non_nullable
+              as int,
+      affiliatedId: freezed == affiliatedId
+          ? _value.affiliatedId
+          : affiliatedId // ignore: cast_nullable_to_non_nullable
+              as int?,
+      affiliatedTo: freezed == affiliatedTo
+          ? _value.affiliatedTo
+          : affiliatedTo // ignore: cast_nullable_to_non_nullable
+              as int?,
+      automatedBy: freezed == automatedBy
+          ? _value.automatedBy
+          : automatedBy // ignore: cast_nullable_to_non_nullable
+              as int?,
+      automatedId: freezed == automatedId
+          ? _value.automatedId
+          : automatedId // ignore: cast_nullable_to_non_nullable
+              as int?,
+      avatar: null == avatar
+          ? _value.avatar
+          : avatar // ignore: cast_nullable_to_non_nullable
+              as String,
+      banner: null == banner
+          ? _value.banner
+          : banner // ignore: cast_nullable_to_non_nullable
+              as String,
+      confirmedAt: freezed == confirmedAt
+          ? _value.confirmedAt
+          : confirmedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      contacts: null == contacts
+          ? _value._contacts
+          : contacts // ignore: cast_nullable_to_non_nullable
+              as List<SnAccountContact>,
+      createdAt: null == createdAt
+          ? _value.createdAt
+          : createdAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      deletedAt: freezed == deletedAt
+          ? _value.deletedAt
+          : deletedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      description: null == description
+          ? _value.description
+          : description // ignore: cast_nullable_to_non_nullable
+              as String,
+      name: null == name
+          ? _value.name
+          : name // ignore: cast_nullable_to_non_nullable
+              as String,
+      nick: null == nick
+          ? _value.nick
+          : nick // ignore: cast_nullable_to_non_nullable
+              as String,
+      permNodes: null == permNodes
+          ? _value._permNodes
+          : permNodes // ignore: cast_nullable_to_non_nullable
+              as Map<String, dynamic>,
+      profile: freezed == profile
+          ? _value.profile
+          : profile // ignore: cast_nullable_to_non_nullable
+              as SnAccountProfile?,
+      suspendedAt: freezed == suspendedAt
+          ? _value.suspendedAt
+          : suspendedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      updatedAt: null == updatedAt
+          ? _value.updatedAt
+          : updatedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+    ));
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$SnAccountImpl implements _SnAccount {
+  const _$SnAccountImpl(
+      {required this.id,
+      required this.affiliatedId,
+      required this.affiliatedTo,
+      required this.automatedBy,
+      required this.automatedId,
+      required this.avatar,
+      required this.banner,
+      required this.confirmedAt,
+      required final List<SnAccountContact> contacts,
+      required this.createdAt,
+      required this.deletedAt,
+      required this.description,
+      required this.name,
+      required this.nick,
+      required final Map<String, dynamic> permNodes,
+      required this.profile,
+      required this.suspendedAt,
+      required this.updatedAt})
+      : _contacts = contacts,
+        _permNodes = permNodes;
+
+  factory _$SnAccountImpl.fromJson(Map<String, dynamic> json) =>
+      _$$SnAccountImplFromJson(json);
+
+  @override
+  final int id;
+  @override
+  final int? affiliatedId;
+  @override
+  final int? affiliatedTo;
+  @override
+  final int? automatedBy;
+  @override
+  final int? automatedId;
+  @override
+  final String avatar;
+  @override
+  final String banner;
+  @override
+  final DateTime? confirmedAt;
+  final List<SnAccountContact> _contacts;
+  @override
+  List<SnAccountContact> get contacts {
+    if (_contacts is EqualUnmodifiableListView) return _contacts;
+    // ignore: implicit_dynamic_type
+    return EqualUnmodifiableListView(_contacts);
+  }
+
+  @override
+  final DateTime createdAt;
+  @override
+  final DateTime? deletedAt;
+  @override
+  final String description;
+  @override
+  final String name;
+  @override
+  final String nick;
+  final Map<String, dynamic> _permNodes;
+  @override
+  Map<String, dynamic> get permNodes {
+    if (_permNodes is EqualUnmodifiableMapView) return _permNodes;
+    // ignore: implicit_dynamic_type
+    return EqualUnmodifiableMapView(_permNodes);
+  }
+
+  @override
+  final SnAccountProfile? profile;
+  @override
+  final DateTime? suspendedAt;
+  @override
+  final DateTime updatedAt;
+
+  @override
+  String toString() {
+    return 'SnAccount(id: $id, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId, avatar: $avatar, banner: $banner, confirmedAt: $confirmedAt, contacts: $contacts, createdAt: $createdAt, deletedAt: $deletedAt, description: $description, name: $name, nick: $nick, permNodes: $permNodes, profile: $profile, suspendedAt: $suspendedAt, updatedAt: $updatedAt)';
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$SnAccountImpl &&
+            (identical(other.id, id) || other.id == id) &&
+            (identical(other.affiliatedId, affiliatedId) ||
+                other.affiliatedId == affiliatedId) &&
+            (identical(other.affiliatedTo, affiliatedTo) ||
+                other.affiliatedTo == affiliatedTo) &&
+            (identical(other.automatedBy, automatedBy) ||
+                other.automatedBy == automatedBy) &&
+            (identical(other.automatedId, automatedId) ||
+                other.automatedId == automatedId) &&
+            (identical(other.avatar, avatar) || other.avatar == avatar) &&
+            (identical(other.banner, banner) || other.banner == banner) &&
+            (identical(other.confirmedAt, confirmedAt) ||
+                other.confirmedAt == confirmedAt) &&
+            const DeepCollectionEquality().equals(other._contacts, _contacts) &&
+            (identical(other.createdAt, createdAt) ||
+                other.createdAt == createdAt) &&
+            (identical(other.deletedAt, deletedAt) ||
+                other.deletedAt == deletedAt) &&
+            (identical(other.description, description) ||
+                other.description == description) &&
+            (identical(other.name, name) || other.name == name) &&
+            (identical(other.nick, nick) || other.nick == nick) &&
+            const DeepCollectionEquality()
+                .equals(other._permNodes, _permNodes) &&
+            (identical(other.profile, profile) || other.profile == profile) &&
+            (identical(other.suspendedAt, suspendedAt) ||
+                other.suspendedAt == suspendedAt) &&
+            (identical(other.updatedAt, updatedAt) ||
+                other.updatedAt == updatedAt));
+  }
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  int get hashCode => Object.hash(
+      runtimeType,
+      id,
+      affiliatedId,
+      affiliatedTo,
+      automatedBy,
+      automatedId,
+      avatar,
+      banner,
+      confirmedAt,
+      const DeepCollectionEquality().hash(_contacts),
+      createdAt,
+      deletedAt,
+      description,
+      name,
+      nick,
+      const DeepCollectionEquality().hash(_permNodes),
+      profile,
+      suspendedAt,
+      updatedAt);
+
+  /// Create a copy of SnAccount
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$SnAccountImplCopyWith<_$SnAccountImpl> get copyWith =>
+      __$$SnAccountImplCopyWithImpl<_$SnAccountImpl>(this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$SnAccountImplToJson(
+      this,
+    );
+  }
+}
+
+abstract class _SnAccount implements SnAccount {
+  const factory _SnAccount(
+      {required final int id,
+      required final int? affiliatedId,
+      required final int? affiliatedTo,
+      required final int? automatedBy,
+      required final int? automatedId,
+      required final String avatar,
+      required final String banner,
+      required final DateTime? confirmedAt,
+      required final List<SnAccountContact> contacts,
+      required final DateTime createdAt,
+      required final DateTime? deletedAt,
+      required final String description,
+      required final String name,
+      required final String nick,
+      required final Map<String, dynamic> permNodes,
+      required final SnAccountProfile? profile,
+      required final DateTime? suspendedAt,
+      required final DateTime updatedAt}) = _$SnAccountImpl;
+
+  factory _SnAccount.fromJson(Map<String, dynamic> json) =
+      _$SnAccountImpl.fromJson;
+
+  @override
+  int get id;
+  @override
+  int? get affiliatedId;
+  @override
+  int? get affiliatedTo;
+  @override
+  int? get automatedBy;
+  @override
+  int? get automatedId;
+  @override
+  String get avatar;
+  @override
+  String get banner;
+  @override
+  DateTime? get confirmedAt;
+  @override
+  List<SnAccountContact> get contacts;
+  @override
+  DateTime get createdAt;
+  @override
+  DateTime? get deletedAt;
+  @override
+  String get description;
+  @override
+  String get name;
+  @override
+  String get nick;
+  @override
+  Map<String, dynamic> get permNodes;
+  @override
+  SnAccountProfile? get profile;
+  @override
+  DateTime? get suspendedAt;
+  @override
+  DateTime get updatedAt;
+
+  /// Create a copy of SnAccount
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  _$$SnAccountImplCopyWith<_$SnAccountImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+SnAccountContact _$SnAccountContactFromJson(Map<String, dynamic> json) {
+  return _SnAccountContact.fromJson(json);
+}
+
+/// @nodoc
+mixin _$SnAccountContact {
+  int get accountId => throw _privateConstructorUsedError;
+  String get content => throw _privateConstructorUsedError;
+  DateTime get createdAt => throw _privateConstructorUsedError;
+  DateTime? get deletedAt => throw _privateConstructorUsedError;
+  int get id => throw _privateConstructorUsedError;
+  bool get isPrimary => throw _privateConstructorUsedError;
+  bool get isPublic => throw _privateConstructorUsedError;
+  int get type => throw _privateConstructorUsedError;
+  DateTime get updatedAt => throw _privateConstructorUsedError;
+  DateTime? get verifiedAt => throw _privateConstructorUsedError;
+
+  /// Serializes this SnAccountContact to a JSON map.
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+
+  /// Create a copy of SnAccountContact
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  $SnAccountContactCopyWith<SnAccountContact> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $SnAccountContactCopyWith<$Res> {
+  factory $SnAccountContactCopyWith(
+          SnAccountContact value, $Res Function(SnAccountContact) then) =
+      _$SnAccountContactCopyWithImpl<$Res, SnAccountContact>;
+  @useResult
+  $Res call(
+      {int accountId,
+      String content,
+      DateTime createdAt,
+      DateTime? deletedAt,
+      int id,
+      bool isPrimary,
+      bool isPublic,
+      int type,
+      DateTime updatedAt,
+      DateTime? verifiedAt});
+}
+
+/// @nodoc
+class _$SnAccountContactCopyWithImpl<$Res, $Val extends SnAccountContact>
+    implements $SnAccountContactCopyWith<$Res> {
+  _$SnAccountContactCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  /// Create a copy of SnAccountContact
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? accountId = null,
+    Object? content = null,
+    Object? createdAt = null,
+    Object? deletedAt = freezed,
+    Object? id = null,
+    Object? isPrimary = null,
+    Object? isPublic = null,
+    Object? type = null,
+    Object? updatedAt = null,
+    Object? verifiedAt = freezed,
+  }) {
+    return _then(_value.copyWith(
+      accountId: null == accountId
+          ? _value.accountId
+          : accountId // ignore: cast_nullable_to_non_nullable
+              as int,
+      content: null == content
+          ? _value.content
+          : content // ignore: cast_nullable_to_non_nullable
+              as String,
+      createdAt: null == createdAt
+          ? _value.createdAt
+          : createdAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      deletedAt: freezed == deletedAt
+          ? _value.deletedAt
+          : deletedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      id: null == id
+          ? _value.id
+          : id // ignore: cast_nullable_to_non_nullable
+              as int,
+      isPrimary: null == isPrimary
+          ? _value.isPrimary
+          : isPrimary // ignore: cast_nullable_to_non_nullable
+              as bool,
+      isPublic: null == isPublic
+          ? _value.isPublic
+          : isPublic // ignore: cast_nullable_to_non_nullable
+              as bool,
+      type: null == type
+          ? _value.type
+          : type // ignore: cast_nullable_to_non_nullable
+              as int,
+      updatedAt: null == updatedAt
+          ? _value.updatedAt
+          : updatedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      verifiedAt: freezed == verifiedAt
+          ? _value.verifiedAt
+          : verifiedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+    ) as $Val);
+  }
+}
+
+/// @nodoc
+abstract class _$$SnAccountContactImplCopyWith<$Res>
+    implements $SnAccountContactCopyWith<$Res> {
+  factory _$$SnAccountContactImplCopyWith(_$SnAccountContactImpl value,
+          $Res Function(_$SnAccountContactImpl) then) =
+      __$$SnAccountContactImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call(
+      {int accountId,
+      String content,
+      DateTime createdAt,
+      DateTime? deletedAt,
+      int id,
+      bool isPrimary,
+      bool isPublic,
+      int type,
+      DateTime updatedAt,
+      DateTime? verifiedAt});
+}
+
+/// @nodoc
+class __$$SnAccountContactImplCopyWithImpl<$Res>
+    extends _$SnAccountContactCopyWithImpl<$Res, _$SnAccountContactImpl>
+    implements _$$SnAccountContactImplCopyWith<$Res> {
+  __$$SnAccountContactImplCopyWithImpl(_$SnAccountContactImpl _value,
+      $Res Function(_$SnAccountContactImpl) _then)
+      : super(_value, _then);
+
+  /// Create a copy of SnAccountContact
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? accountId = null,
+    Object? content = null,
+    Object? createdAt = null,
+    Object? deletedAt = freezed,
+    Object? id = null,
+    Object? isPrimary = null,
+    Object? isPublic = null,
+    Object? type = null,
+    Object? updatedAt = null,
+    Object? verifiedAt = freezed,
+  }) {
+    return _then(_$SnAccountContactImpl(
+      accountId: null == accountId
+          ? _value.accountId
+          : accountId // ignore: cast_nullable_to_non_nullable
+              as int,
+      content: null == content
+          ? _value.content
+          : content // ignore: cast_nullable_to_non_nullable
+              as String,
+      createdAt: null == createdAt
+          ? _value.createdAt
+          : createdAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      deletedAt: freezed == deletedAt
+          ? _value.deletedAt
+          : deletedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      id: null == id
+          ? _value.id
+          : id // ignore: cast_nullable_to_non_nullable
+              as int,
+      isPrimary: null == isPrimary
+          ? _value.isPrimary
+          : isPrimary // ignore: cast_nullable_to_non_nullable
+              as bool,
+      isPublic: null == isPublic
+          ? _value.isPublic
+          : isPublic // ignore: cast_nullable_to_non_nullable
+              as bool,
+      type: null == type
+          ? _value.type
+          : type // ignore: cast_nullable_to_non_nullable
+              as int,
+      updatedAt: null == updatedAt
+          ? _value.updatedAt
+          : updatedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      verifiedAt: freezed == verifiedAt
+          ? _value.verifiedAt
+          : verifiedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+    ));
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$SnAccountContactImpl implements _SnAccountContact {
+  const _$SnAccountContactImpl(
+      {required this.accountId,
+      required this.content,
+      required this.createdAt,
+      required this.deletedAt,
+      required this.id,
+      required this.isPrimary,
+      required this.isPublic,
+      required this.type,
+      required this.updatedAt,
+      required this.verifiedAt});
+
+  factory _$SnAccountContactImpl.fromJson(Map<String, dynamic> json) =>
+      _$$SnAccountContactImplFromJson(json);
+
+  @override
+  final int accountId;
+  @override
+  final String content;
+  @override
+  final DateTime createdAt;
+  @override
+  final DateTime? deletedAt;
+  @override
+  final int id;
+  @override
+  final bool isPrimary;
+  @override
+  final bool isPublic;
+  @override
+  final int type;
+  @override
+  final DateTime updatedAt;
+  @override
+  final DateTime? verifiedAt;
+
+  @override
+  String toString() {
+    return 'SnAccountContact(accountId: $accountId, content: $content, createdAt: $createdAt, deletedAt: $deletedAt, id: $id, isPrimary: $isPrimary, isPublic: $isPublic, type: $type, updatedAt: $updatedAt, verifiedAt: $verifiedAt)';
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$SnAccountContactImpl &&
+            (identical(other.accountId, accountId) ||
+                other.accountId == accountId) &&
+            (identical(other.content, content) || other.content == content) &&
+            (identical(other.createdAt, createdAt) ||
+                other.createdAt == createdAt) &&
+            (identical(other.deletedAt, deletedAt) ||
+                other.deletedAt == deletedAt) &&
+            (identical(other.id, id) || other.id == id) &&
+            (identical(other.isPrimary, isPrimary) ||
+                other.isPrimary == isPrimary) &&
+            (identical(other.isPublic, isPublic) ||
+                other.isPublic == isPublic) &&
+            (identical(other.type, type) || other.type == type) &&
+            (identical(other.updatedAt, updatedAt) ||
+                other.updatedAt == updatedAt) &&
+            (identical(other.verifiedAt, verifiedAt) ||
+                other.verifiedAt == verifiedAt));
+  }
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  int get hashCode => Object.hash(runtimeType, accountId, content, createdAt,
+      deletedAt, id, isPrimary, isPublic, type, updatedAt, verifiedAt);
+
+  /// Create a copy of SnAccountContact
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$SnAccountContactImplCopyWith<_$SnAccountContactImpl> get copyWith =>
+      __$$SnAccountContactImplCopyWithImpl<_$SnAccountContactImpl>(
+          this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$SnAccountContactImplToJson(
+      this,
+    );
+  }
+}
+
+abstract class _SnAccountContact implements SnAccountContact {
+  const factory _SnAccountContact(
+      {required final int accountId,
+      required final String content,
+      required final DateTime createdAt,
+      required final DateTime? deletedAt,
+      required final int id,
+      required final bool isPrimary,
+      required final bool isPublic,
+      required final int type,
+      required final DateTime updatedAt,
+      required final DateTime? verifiedAt}) = _$SnAccountContactImpl;
+
+  factory _SnAccountContact.fromJson(Map<String, dynamic> json) =
+      _$SnAccountContactImpl.fromJson;
+
+  @override
+  int get accountId;
+  @override
+  String get content;
+  @override
+  DateTime get createdAt;
+  @override
+  DateTime? get deletedAt;
+  @override
+  int get id;
+  @override
+  bool get isPrimary;
+  @override
+  bool get isPublic;
+  @override
+  int get type;
+  @override
+  DateTime get updatedAt;
+  @override
+  DateTime? get verifiedAt;
+
+  /// Create a copy of SnAccountContact
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  _$$SnAccountContactImplCopyWith<_$SnAccountContactImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) {
+  return _SnAccountProfile.fromJson(json);
+}
+
+/// @nodoc
+mixin _$SnAccountProfile {
+  int get id => throw _privateConstructorUsedError;
+  int get accountId => throw _privateConstructorUsedError;
+  DateTime? get birthday => throw _privateConstructorUsedError;
+  DateTime get createdAt => throw _privateConstructorUsedError;
+  DateTime? get deletedAt => throw _privateConstructorUsedError;
+  int get experience => throw _privateConstructorUsedError;
+  String get firstName => throw _privateConstructorUsedError;
+  String get lastName => throw _privateConstructorUsedError;
+  DateTime? get lastSeenAt => throw _privateConstructorUsedError;
+  DateTime get updatedAt => throw _privateConstructorUsedError;
+
+  /// Serializes this SnAccountProfile to a JSON map.
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+
+  /// Create a copy of SnAccountProfile
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  $SnAccountProfileCopyWith<SnAccountProfile> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $SnAccountProfileCopyWith<$Res> {
+  factory $SnAccountProfileCopyWith(
+          SnAccountProfile value, $Res Function(SnAccountProfile) then) =
+      _$SnAccountProfileCopyWithImpl<$Res, SnAccountProfile>;
+  @useResult
+  $Res call(
+      {int id,
+      int accountId,
+      DateTime? birthday,
+      DateTime createdAt,
+      DateTime? deletedAt,
+      int experience,
+      String firstName,
+      String lastName,
+      DateTime? lastSeenAt,
+      DateTime updatedAt});
+}
+
+/// @nodoc
+class _$SnAccountProfileCopyWithImpl<$Res, $Val extends SnAccountProfile>
+    implements $SnAccountProfileCopyWith<$Res> {
+  _$SnAccountProfileCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  /// Create a copy of SnAccountProfile
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? id = null,
+    Object? accountId = null,
+    Object? birthday = freezed,
+    Object? createdAt = null,
+    Object? deletedAt = freezed,
+    Object? experience = null,
+    Object? firstName = null,
+    Object? lastName = null,
+    Object? lastSeenAt = freezed,
+    Object? updatedAt = null,
+  }) {
+    return _then(_value.copyWith(
+      id: null == id
+          ? _value.id
+          : id // ignore: cast_nullable_to_non_nullable
+              as int,
+      accountId: null == accountId
+          ? _value.accountId
+          : accountId // ignore: cast_nullable_to_non_nullable
+              as int,
+      birthday: freezed == birthday
+          ? _value.birthday
+          : birthday // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      createdAt: null == createdAt
+          ? _value.createdAt
+          : createdAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      deletedAt: freezed == deletedAt
+          ? _value.deletedAt
+          : deletedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      experience: null == experience
+          ? _value.experience
+          : experience // ignore: cast_nullable_to_non_nullable
+              as int,
+      firstName: null == firstName
+          ? _value.firstName
+          : firstName // ignore: cast_nullable_to_non_nullable
+              as String,
+      lastName: null == lastName
+          ? _value.lastName
+          : lastName // ignore: cast_nullable_to_non_nullable
+              as String,
+      lastSeenAt: freezed == lastSeenAt
+          ? _value.lastSeenAt
+          : lastSeenAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      updatedAt: null == updatedAt
+          ? _value.updatedAt
+          : updatedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+    ) as $Val);
+  }
+}
+
+/// @nodoc
+abstract class _$$SnAccountProfileImplCopyWith<$Res>
+    implements $SnAccountProfileCopyWith<$Res> {
+  factory _$$SnAccountProfileImplCopyWith(_$SnAccountProfileImpl value,
+          $Res Function(_$SnAccountProfileImpl) then) =
+      __$$SnAccountProfileImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call(
+      {int id,
+      int accountId,
+      DateTime? birthday,
+      DateTime createdAt,
+      DateTime? deletedAt,
+      int experience,
+      String firstName,
+      String lastName,
+      DateTime? lastSeenAt,
+      DateTime updatedAt});
+}
+
+/// @nodoc
+class __$$SnAccountProfileImplCopyWithImpl<$Res>
+    extends _$SnAccountProfileCopyWithImpl<$Res, _$SnAccountProfileImpl>
+    implements _$$SnAccountProfileImplCopyWith<$Res> {
+  __$$SnAccountProfileImplCopyWithImpl(_$SnAccountProfileImpl _value,
+      $Res Function(_$SnAccountProfileImpl) _then)
+      : super(_value, _then);
+
+  /// Create a copy of SnAccountProfile
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? id = null,
+    Object? accountId = null,
+    Object? birthday = freezed,
+    Object? createdAt = null,
+    Object? deletedAt = freezed,
+    Object? experience = null,
+    Object? firstName = null,
+    Object? lastName = null,
+    Object? lastSeenAt = freezed,
+    Object? updatedAt = null,
+  }) {
+    return _then(_$SnAccountProfileImpl(
+      id: null == id
+          ? _value.id
+          : id // ignore: cast_nullable_to_non_nullable
+              as int,
+      accountId: null == accountId
+          ? _value.accountId
+          : accountId // ignore: cast_nullable_to_non_nullable
+              as int,
+      birthday: freezed == birthday
+          ? _value.birthday
+          : birthday // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      createdAt: null == createdAt
+          ? _value.createdAt
+          : createdAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      deletedAt: freezed == deletedAt
+          ? _value.deletedAt
+          : deletedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      experience: null == experience
+          ? _value.experience
+          : experience // ignore: cast_nullable_to_non_nullable
+              as int,
+      firstName: null == firstName
+          ? _value.firstName
+          : firstName // ignore: cast_nullable_to_non_nullable
+              as String,
+      lastName: null == lastName
+          ? _value.lastName
+          : lastName // ignore: cast_nullable_to_non_nullable
+              as String,
+      lastSeenAt: freezed == lastSeenAt
+          ? _value.lastSeenAt
+          : lastSeenAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      updatedAt: null == updatedAt
+          ? _value.updatedAt
+          : updatedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+    ));
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$SnAccountProfileImpl implements _SnAccountProfile {
+  const _$SnAccountProfileImpl(
+      {required this.id,
+      required this.accountId,
+      required this.birthday,
+      required this.createdAt,
+      required this.deletedAt,
+      required this.experience,
+      required this.firstName,
+      required this.lastName,
+      required this.lastSeenAt,
+      required this.updatedAt});
+
+  factory _$SnAccountProfileImpl.fromJson(Map<String, dynamic> json) =>
+      _$$SnAccountProfileImplFromJson(json);
+
+  @override
+  final int id;
+  @override
+  final int accountId;
+  @override
+  final DateTime? birthday;
+  @override
+  final DateTime createdAt;
+  @override
+  final DateTime? deletedAt;
+  @override
+  final int experience;
+  @override
+  final String firstName;
+  @override
+  final String lastName;
+  @override
+  final DateTime? lastSeenAt;
+  @override
+  final DateTime updatedAt;
+
+  @override
+  String toString() {
+    return 'SnAccountProfile(id: $id, accountId: $accountId, birthday: $birthday, createdAt: $createdAt, deletedAt: $deletedAt, experience: $experience, firstName: $firstName, lastName: $lastName, lastSeenAt: $lastSeenAt, updatedAt: $updatedAt)';
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$SnAccountProfileImpl &&
+            (identical(other.id, id) || other.id == id) &&
+            (identical(other.accountId, accountId) ||
+                other.accountId == accountId) &&
+            (identical(other.birthday, birthday) ||
+                other.birthday == birthday) &&
+            (identical(other.createdAt, createdAt) ||
+                other.createdAt == createdAt) &&
+            (identical(other.deletedAt, deletedAt) ||
+                other.deletedAt == deletedAt) &&
+            (identical(other.experience, experience) ||
+                other.experience == experience) &&
+            (identical(other.firstName, firstName) ||
+                other.firstName == firstName) &&
+            (identical(other.lastName, lastName) ||
+                other.lastName == lastName) &&
+            (identical(other.lastSeenAt, lastSeenAt) ||
+                other.lastSeenAt == lastSeenAt) &&
+            (identical(other.updatedAt, updatedAt) ||
+                other.updatedAt == updatedAt));
+  }
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  int get hashCode => Object.hash(
+      runtimeType,
+      id,
+      accountId,
+      birthday,
+      createdAt,
+      deletedAt,
+      experience,
+      firstName,
+      lastName,
+      lastSeenAt,
+      updatedAt);
+
+  /// Create a copy of SnAccountProfile
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$SnAccountProfileImplCopyWith<_$SnAccountProfileImpl> get copyWith =>
+      __$$SnAccountProfileImplCopyWithImpl<_$SnAccountProfileImpl>(
+          this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$SnAccountProfileImplToJson(
+      this,
+    );
+  }
+}
+
+abstract class _SnAccountProfile implements SnAccountProfile {
+  const factory _SnAccountProfile(
+      {required final int id,
+      required final int accountId,
+      required final DateTime? birthday,
+      required final DateTime createdAt,
+      required final DateTime? deletedAt,
+      required final int experience,
+      required final String firstName,
+      required final String lastName,
+      required final DateTime? lastSeenAt,
+      required final DateTime updatedAt}) = _$SnAccountProfileImpl;
+
+  factory _SnAccountProfile.fromJson(Map<String, dynamic> json) =
+      _$SnAccountProfileImpl.fromJson;
+
+  @override
+  int get id;
+  @override
+  int get accountId;
+  @override
+  DateTime? get birthday;
+  @override
+  DateTime get createdAt;
+  @override
+  DateTime? get deletedAt;
+  @override
+  int get experience;
+  @override
+  String get firstName;
+  @override
+  String get lastName;
+  @override
+  DateTime? get lastSeenAt;
+  @override
+  DateTime get updatedAt;
+
+  /// Create a copy of SnAccountProfile
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  _$$SnAccountProfileImplCopyWith<_$SnAccountProfileImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}
diff --git a/lib/types/account.g.dart b/lib/types/account.g.dart
new file mode 100644
index 0000000..bbf2f82
--- /dev/null
+++ b/lib/types/account.g.dart
@@ -0,0 +1,131 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'account.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
+    _$SnAccountImpl(
+      id: (json['id'] as num).toInt(),
+      affiliatedId: (json['affiliated_id'] as num?)?.toInt(),
+      affiliatedTo: (json['affiliated_to'] as num?)?.toInt(),
+      automatedBy: (json['automated_by'] as num?)?.toInt(),
+      automatedId: (json['automated_id'] as num?)?.toInt(),
+      avatar: json['avatar'] as String,
+      banner: json['banner'] as String,
+      confirmedAt: json['confirmed_at'] == null
+          ? null
+          : DateTime.parse(json['confirmed_at'] as String),
+      contacts: (json['contacts'] as List<dynamic>)
+          .map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>))
+          .toList(),
+      createdAt: DateTime.parse(json['created_at'] as String),
+      deletedAt: json['deleted_at'] == null
+          ? null
+          : DateTime.parse(json['deleted_at'] as String),
+      description: json['description'] as String,
+      name: json['name'] as String,
+      nick: json['nick'] as String,
+      permNodes: json['perm_nodes'] as Map<String, dynamic>,
+      profile: json['profile'] == null
+          ? null
+          : SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
+      suspendedAt: json['suspended_at'] == null
+          ? null
+          : DateTime.parse(json['suspended_at'] as String),
+      updatedAt: DateTime.parse(json['updated_at'] as String),
+    );
+
+Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) =>
+    <String, dynamic>{
+      'id': instance.id,
+      'affiliated_id': instance.affiliatedId,
+      'affiliated_to': instance.affiliatedTo,
+      'automated_by': instance.automatedBy,
+      'automated_id': instance.automatedId,
+      'avatar': instance.avatar,
+      'banner': instance.banner,
+      'confirmed_at': instance.confirmedAt?.toIso8601String(),
+      'contacts': instance.contacts.map((e) => e.toJson()).toList(),
+      'created_at': instance.createdAt.toIso8601String(),
+      'deleted_at': instance.deletedAt?.toIso8601String(),
+      'description': instance.description,
+      'name': instance.name,
+      'nick': instance.nick,
+      'perm_nodes': instance.permNodes,
+      'profile': instance.profile?.toJson(),
+      'suspended_at': instance.suspendedAt?.toIso8601String(),
+      'updated_at': instance.updatedAt.toIso8601String(),
+    };
+
+_$SnAccountContactImpl _$$SnAccountContactImplFromJson(
+        Map<String, dynamic> json) =>
+    _$SnAccountContactImpl(
+      accountId: (json['account_id'] as num).toInt(),
+      content: json['content'] as String,
+      createdAt: DateTime.parse(json['created_at'] as String),
+      deletedAt: json['deleted_at'] == null
+          ? null
+          : DateTime.parse(json['deleted_at'] as String),
+      id: (json['id'] as num).toInt(),
+      isPrimary: json['is_primary'] as bool,
+      isPublic: json['is_public'] as bool,
+      type: (json['type'] as num).toInt(),
+      updatedAt: DateTime.parse(json['updated_at'] as String),
+      verifiedAt: json['verified_at'] == null
+          ? null
+          : DateTime.parse(json['verified_at'] as String),
+    );
+
+Map<String, dynamic> _$$SnAccountContactImplToJson(
+        _$SnAccountContactImpl instance) =>
+    <String, dynamic>{
+      'account_id': instance.accountId,
+      'content': instance.content,
+      'created_at': instance.createdAt.toIso8601String(),
+      'deleted_at': instance.deletedAt?.toIso8601String(),
+      'id': instance.id,
+      'is_primary': instance.isPrimary,
+      'is_public': instance.isPublic,
+      'type': instance.type,
+      'updated_at': instance.updatedAt.toIso8601String(),
+      'verified_at': instance.verifiedAt?.toIso8601String(),
+    };
+
+_$SnAccountProfileImpl _$$SnAccountProfileImplFromJson(
+        Map<String, dynamic> json) =>
+    _$SnAccountProfileImpl(
+      id: (json['id'] as num).toInt(),
+      accountId: (json['account_id'] as num).toInt(),
+      birthday: json['birthday'] == null
+          ? null
+          : DateTime.parse(json['birthday'] as String),
+      createdAt: DateTime.parse(json['created_at'] as String),
+      deletedAt: json['deleted_at'] == null
+          ? null
+          : DateTime.parse(json['deleted_at'] as String),
+      experience: (json['experience'] as num).toInt(),
+      firstName: json['first_name'] as String,
+      lastName: json['last_name'] as String,
+      lastSeenAt: json['last_seen_at'] == null
+          ? null
+          : DateTime.parse(json['last_seen_at'] as String),
+      updatedAt: DateTime.parse(json['updated_at'] as String),
+    );
+
+Map<String, dynamic> _$$SnAccountProfileImplToJson(
+        _$SnAccountProfileImpl instance) =>
+    <String, dynamic>{
+      'id': instance.id,
+      'account_id': instance.accountId,
+      'birthday': instance.birthday?.toIso8601String(),
+      'created_at': instance.createdAt.toIso8601String(),
+      'deleted_at': instance.deletedAt?.toIso8601String(),
+      'experience': instance.experience,
+      'first_name': instance.firstName,
+      'last_name': instance.lastName,
+      'last_seen_at': instance.lastSeenAt?.toIso8601String(),
+      'updated_at': instance.updatedAt.toIso8601String(),
+    };
diff --git a/lib/types/auth.dart b/lib/types/auth.dart
new file mode 100644
index 0000000..5bcdead
--- /dev/null
+++ b/lib/types/auth.dart
@@ -0,0 +1,57 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'auth.freezed.dart';
+part 'auth.g.dart';
+
+@freezed
+class SnAuthResult with _$SnAuthResult {
+  const factory SnAuthResult({
+    required bool isFinished,
+    required SnAuthTicket? ticket,
+  }) = _SnAuthResult;
+
+  factory SnAuthResult.fromJson(Map<String, Object?> json) =>
+      _$SnAuthResultFromJson(json);
+}
+
+@freezed
+class SnAuthTicket with _$SnAuthTicket {
+  const factory SnAuthTicket({
+    required int id,
+    required DateTime createdAt,
+    required DateTime updatedAt,
+    required DateTime? deletedAt,
+    required int stepRemain,
+    required String? grantToken,
+    required String? accessToken,
+    required String? refreshToken,
+    required String ipAddress,
+    required String location,
+    required String userAgent,
+    required DateTime? expiredAt,
+    required DateTime? lastGrantAt,
+    required DateTime? availableAt,
+    required String? nonce,
+    required int? accountId,
+    @Default([]) List<int> factorTrail,
+  }) = _SnAuthTicket;
+
+  factory SnAuthTicket.fromJson(Map<String, Object?> json) =>
+      _$SnAuthTicketFromJson(json);
+}
+
+@freezed
+class SnAuthFactor with _$SnAuthFactor {
+  const factory SnAuthFactor({
+    required int id,
+    required DateTime createdAt,
+    required DateTime updatedAt,
+    required DateTime? deletedAt,
+    required int type,
+    required Map<String, dynamic>? config,
+    required int? accountId,
+  }) = _SnAuthFactor;
+
+  factory SnAuthFactor.fromJson(Map<String, Object?> json) =>
+      _$SnAuthFactorFromJson(json);
+}
diff --git a/lib/types/auth.freezed.dart b/lib/types/auth.freezed.dart
new file mode 100644
index 0000000..8fda77e
--- /dev/null
+++ b/lib/types/auth.freezed.dart
@@ -0,0 +1,1002 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'auth.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity<T>(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+    'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
+
+SnAuthResult _$SnAuthResultFromJson(Map<String, dynamic> json) {
+  return _SnAuthResult.fromJson(json);
+}
+
+/// @nodoc
+mixin _$SnAuthResult {
+  bool get isFinished => throw _privateConstructorUsedError;
+  SnAuthTicket? get ticket => throw _privateConstructorUsedError;
+
+  /// Serializes this SnAuthResult to a JSON map.
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+
+  /// Create a copy of SnAuthResult
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  $SnAuthResultCopyWith<SnAuthResult> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $SnAuthResultCopyWith<$Res> {
+  factory $SnAuthResultCopyWith(
+          SnAuthResult value, $Res Function(SnAuthResult) then) =
+      _$SnAuthResultCopyWithImpl<$Res, SnAuthResult>;
+  @useResult
+  $Res call({bool isFinished, SnAuthTicket? ticket});
+
+  $SnAuthTicketCopyWith<$Res>? get ticket;
+}
+
+/// @nodoc
+class _$SnAuthResultCopyWithImpl<$Res, $Val extends SnAuthResult>
+    implements $SnAuthResultCopyWith<$Res> {
+  _$SnAuthResultCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  /// Create a copy of SnAuthResult
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? isFinished = null,
+    Object? ticket = freezed,
+  }) {
+    return _then(_value.copyWith(
+      isFinished: null == isFinished
+          ? _value.isFinished
+          : isFinished // ignore: cast_nullable_to_non_nullable
+              as bool,
+      ticket: freezed == ticket
+          ? _value.ticket
+          : ticket // ignore: cast_nullable_to_non_nullable
+              as SnAuthTicket?,
+    ) as $Val);
+  }
+
+  /// Create a copy of SnAuthResult
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @pragma('vm:prefer-inline')
+  $SnAuthTicketCopyWith<$Res>? get ticket {
+    if (_value.ticket == null) {
+      return null;
+    }
+
+    return $SnAuthTicketCopyWith<$Res>(_value.ticket!, (value) {
+      return _then(_value.copyWith(ticket: value) as $Val);
+    });
+  }
+}
+
+/// @nodoc
+abstract class _$$SnAuthResultImplCopyWith<$Res>
+    implements $SnAuthResultCopyWith<$Res> {
+  factory _$$SnAuthResultImplCopyWith(
+          _$SnAuthResultImpl value, $Res Function(_$SnAuthResultImpl) then) =
+      __$$SnAuthResultImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call({bool isFinished, SnAuthTicket? ticket});
+
+  @override
+  $SnAuthTicketCopyWith<$Res>? get ticket;
+}
+
+/// @nodoc
+class __$$SnAuthResultImplCopyWithImpl<$Res>
+    extends _$SnAuthResultCopyWithImpl<$Res, _$SnAuthResultImpl>
+    implements _$$SnAuthResultImplCopyWith<$Res> {
+  __$$SnAuthResultImplCopyWithImpl(
+      _$SnAuthResultImpl _value, $Res Function(_$SnAuthResultImpl) _then)
+      : super(_value, _then);
+
+  /// Create a copy of SnAuthResult
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? isFinished = null,
+    Object? ticket = freezed,
+  }) {
+    return _then(_$SnAuthResultImpl(
+      isFinished: null == isFinished
+          ? _value.isFinished
+          : isFinished // ignore: cast_nullable_to_non_nullable
+              as bool,
+      ticket: freezed == ticket
+          ? _value.ticket
+          : ticket // ignore: cast_nullable_to_non_nullable
+              as SnAuthTicket?,
+    ));
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$SnAuthResultImpl implements _SnAuthResult {
+  const _$SnAuthResultImpl({required this.isFinished, required this.ticket});
+
+  factory _$SnAuthResultImpl.fromJson(Map<String, dynamic> json) =>
+      _$$SnAuthResultImplFromJson(json);
+
+  @override
+  final bool isFinished;
+  @override
+  final SnAuthTicket? ticket;
+
+  @override
+  String toString() {
+    return 'SnAuthResult(isFinished: $isFinished, ticket: $ticket)';
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$SnAuthResultImpl &&
+            (identical(other.isFinished, isFinished) ||
+                other.isFinished == isFinished) &&
+            (identical(other.ticket, ticket) || other.ticket == ticket));
+  }
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  int get hashCode => Object.hash(runtimeType, isFinished, ticket);
+
+  /// Create a copy of SnAuthResult
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$SnAuthResultImplCopyWith<_$SnAuthResultImpl> get copyWith =>
+      __$$SnAuthResultImplCopyWithImpl<_$SnAuthResultImpl>(this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$SnAuthResultImplToJson(
+      this,
+    );
+  }
+}
+
+abstract class _SnAuthResult implements SnAuthResult {
+  const factory _SnAuthResult(
+      {required final bool isFinished,
+      required final SnAuthTicket? ticket}) = _$SnAuthResultImpl;
+
+  factory _SnAuthResult.fromJson(Map<String, dynamic> json) =
+      _$SnAuthResultImpl.fromJson;
+
+  @override
+  bool get isFinished;
+  @override
+  SnAuthTicket? get ticket;
+
+  /// Create a copy of SnAuthResult
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  _$$SnAuthResultImplCopyWith<_$SnAuthResultImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+SnAuthTicket _$SnAuthTicketFromJson(Map<String, dynamic> json) {
+  return _SnAuthTicket.fromJson(json);
+}
+
+/// @nodoc
+mixin _$SnAuthTicket {
+  int get id => throw _privateConstructorUsedError;
+  DateTime get createdAt => throw _privateConstructorUsedError;
+  DateTime get updatedAt => throw _privateConstructorUsedError;
+  DateTime? get deletedAt => throw _privateConstructorUsedError;
+  int get stepRemain => throw _privateConstructorUsedError;
+  String? get grantToken => throw _privateConstructorUsedError;
+  String? get accessToken => throw _privateConstructorUsedError;
+  String? get refreshToken => throw _privateConstructorUsedError;
+  String get ipAddress => throw _privateConstructorUsedError;
+  String get location => throw _privateConstructorUsedError;
+  String get userAgent => throw _privateConstructorUsedError;
+  DateTime? get expiredAt => throw _privateConstructorUsedError;
+  DateTime? get lastGrantAt => throw _privateConstructorUsedError;
+  DateTime? get availableAt => throw _privateConstructorUsedError;
+  String? get nonce => throw _privateConstructorUsedError;
+  int? get accountId => throw _privateConstructorUsedError;
+  List<int> get factorTrail => throw _privateConstructorUsedError;
+
+  /// Serializes this SnAuthTicket to a JSON map.
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+
+  /// Create a copy of SnAuthTicket
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  $SnAuthTicketCopyWith<SnAuthTicket> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $SnAuthTicketCopyWith<$Res> {
+  factory $SnAuthTicketCopyWith(
+          SnAuthTicket value, $Res Function(SnAuthTicket) then) =
+      _$SnAuthTicketCopyWithImpl<$Res, SnAuthTicket>;
+  @useResult
+  $Res call(
+      {int id,
+      DateTime createdAt,
+      DateTime updatedAt,
+      DateTime? deletedAt,
+      int stepRemain,
+      String? grantToken,
+      String? accessToken,
+      String? refreshToken,
+      String ipAddress,
+      String location,
+      String userAgent,
+      DateTime? expiredAt,
+      DateTime? lastGrantAt,
+      DateTime? availableAt,
+      String? nonce,
+      int? accountId,
+      List<int> factorTrail});
+}
+
+/// @nodoc
+class _$SnAuthTicketCopyWithImpl<$Res, $Val extends SnAuthTicket>
+    implements $SnAuthTicketCopyWith<$Res> {
+  _$SnAuthTicketCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  /// Create a copy of SnAuthTicket
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? id = null,
+    Object? createdAt = null,
+    Object? updatedAt = null,
+    Object? deletedAt = freezed,
+    Object? stepRemain = null,
+    Object? grantToken = freezed,
+    Object? accessToken = freezed,
+    Object? refreshToken = freezed,
+    Object? ipAddress = null,
+    Object? location = null,
+    Object? userAgent = null,
+    Object? expiredAt = freezed,
+    Object? lastGrantAt = freezed,
+    Object? availableAt = freezed,
+    Object? nonce = freezed,
+    Object? accountId = freezed,
+    Object? factorTrail = null,
+  }) {
+    return _then(_value.copyWith(
+      id: null == id
+          ? _value.id
+          : id // ignore: cast_nullable_to_non_nullable
+              as int,
+      createdAt: null == createdAt
+          ? _value.createdAt
+          : createdAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      updatedAt: null == updatedAt
+          ? _value.updatedAt
+          : updatedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      deletedAt: freezed == deletedAt
+          ? _value.deletedAt
+          : deletedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      stepRemain: null == stepRemain
+          ? _value.stepRemain
+          : stepRemain // ignore: cast_nullable_to_non_nullable
+              as int,
+      grantToken: freezed == grantToken
+          ? _value.grantToken
+          : grantToken // ignore: cast_nullable_to_non_nullable
+              as String?,
+      accessToken: freezed == accessToken
+          ? _value.accessToken
+          : accessToken // ignore: cast_nullable_to_non_nullable
+              as String?,
+      refreshToken: freezed == refreshToken
+          ? _value.refreshToken
+          : refreshToken // ignore: cast_nullable_to_non_nullable
+              as String?,
+      ipAddress: null == ipAddress
+          ? _value.ipAddress
+          : ipAddress // ignore: cast_nullable_to_non_nullable
+              as String,
+      location: null == location
+          ? _value.location
+          : location // ignore: cast_nullable_to_non_nullable
+              as String,
+      userAgent: null == userAgent
+          ? _value.userAgent
+          : userAgent // ignore: cast_nullable_to_non_nullable
+              as String,
+      expiredAt: freezed == expiredAt
+          ? _value.expiredAt
+          : expiredAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      lastGrantAt: freezed == lastGrantAt
+          ? _value.lastGrantAt
+          : lastGrantAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      availableAt: freezed == availableAt
+          ? _value.availableAt
+          : availableAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      nonce: freezed == nonce
+          ? _value.nonce
+          : nonce // ignore: cast_nullable_to_non_nullable
+              as String?,
+      accountId: freezed == accountId
+          ? _value.accountId
+          : accountId // ignore: cast_nullable_to_non_nullable
+              as int?,
+      factorTrail: null == factorTrail
+          ? _value.factorTrail
+          : factorTrail // ignore: cast_nullable_to_non_nullable
+              as List<int>,
+    ) as $Val);
+  }
+}
+
+/// @nodoc
+abstract class _$$SnAuthTicketImplCopyWith<$Res>
+    implements $SnAuthTicketCopyWith<$Res> {
+  factory _$$SnAuthTicketImplCopyWith(
+          _$SnAuthTicketImpl value, $Res Function(_$SnAuthTicketImpl) then) =
+      __$$SnAuthTicketImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call(
+      {int id,
+      DateTime createdAt,
+      DateTime updatedAt,
+      DateTime? deletedAt,
+      int stepRemain,
+      String? grantToken,
+      String? accessToken,
+      String? refreshToken,
+      String ipAddress,
+      String location,
+      String userAgent,
+      DateTime? expiredAt,
+      DateTime? lastGrantAt,
+      DateTime? availableAt,
+      String? nonce,
+      int? accountId,
+      List<int> factorTrail});
+}
+
+/// @nodoc
+class __$$SnAuthTicketImplCopyWithImpl<$Res>
+    extends _$SnAuthTicketCopyWithImpl<$Res, _$SnAuthTicketImpl>
+    implements _$$SnAuthTicketImplCopyWith<$Res> {
+  __$$SnAuthTicketImplCopyWithImpl(
+      _$SnAuthTicketImpl _value, $Res Function(_$SnAuthTicketImpl) _then)
+      : super(_value, _then);
+
+  /// Create a copy of SnAuthTicket
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? id = null,
+    Object? createdAt = null,
+    Object? updatedAt = null,
+    Object? deletedAt = freezed,
+    Object? stepRemain = null,
+    Object? grantToken = freezed,
+    Object? accessToken = freezed,
+    Object? refreshToken = freezed,
+    Object? ipAddress = null,
+    Object? location = null,
+    Object? userAgent = null,
+    Object? expiredAt = freezed,
+    Object? lastGrantAt = freezed,
+    Object? availableAt = freezed,
+    Object? nonce = freezed,
+    Object? accountId = freezed,
+    Object? factorTrail = null,
+  }) {
+    return _then(_$SnAuthTicketImpl(
+      id: null == id
+          ? _value.id
+          : id // ignore: cast_nullable_to_non_nullable
+              as int,
+      createdAt: null == createdAt
+          ? _value.createdAt
+          : createdAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      updatedAt: null == updatedAt
+          ? _value.updatedAt
+          : updatedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      deletedAt: freezed == deletedAt
+          ? _value.deletedAt
+          : deletedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      stepRemain: null == stepRemain
+          ? _value.stepRemain
+          : stepRemain // ignore: cast_nullable_to_non_nullable
+              as int,
+      grantToken: freezed == grantToken
+          ? _value.grantToken
+          : grantToken // ignore: cast_nullable_to_non_nullable
+              as String?,
+      accessToken: freezed == accessToken
+          ? _value.accessToken
+          : accessToken // ignore: cast_nullable_to_non_nullable
+              as String?,
+      refreshToken: freezed == refreshToken
+          ? _value.refreshToken
+          : refreshToken // ignore: cast_nullable_to_non_nullable
+              as String?,
+      ipAddress: null == ipAddress
+          ? _value.ipAddress
+          : ipAddress // ignore: cast_nullable_to_non_nullable
+              as String,
+      location: null == location
+          ? _value.location
+          : location // ignore: cast_nullable_to_non_nullable
+              as String,
+      userAgent: null == userAgent
+          ? _value.userAgent
+          : userAgent // ignore: cast_nullable_to_non_nullable
+              as String,
+      expiredAt: freezed == expiredAt
+          ? _value.expiredAt
+          : expiredAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      lastGrantAt: freezed == lastGrantAt
+          ? _value.lastGrantAt
+          : lastGrantAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      availableAt: freezed == availableAt
+          ? _value.availableAt
+          : availableAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      nonce: freezed == nonce
+          ? _value.nonce
+          : nonce // ignore: cast_nullable_to_non_nullable
+              as String?,
+      accountId: freezed == accountId
+          ? _value.accountId
+          : accountId // ignore: cast_nullable_to_non_nullable
+              as int?,
+      factorTrail: null == factorTrail
+          ? _value._factorTrail
+          : factorTrail // ignore: cast_nullable_to_non_nullable
+              as List<int>,
+    ));
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$SnAuthTicketImpl implements _SnAuthTicket {
+  const _$SnAuthTicketImpl(
+      {required this.id,
+      required this.createdAt,
+      required this.updatedAt,
+      required this.deletedAt,
+      required this.stepRemain,
+      required this.grantToken,
+      required this.accessToken,
+      required this.refreshToken,
+      required this.ipAddress,
+      required this.location,
+      required this.userAgent,
+      required this.expiredAt,
+      required this.lastGrantAt,
+      required this.availableAt,
+      required this.nonce,
+      required this.accountId,
+      final List<int> factorTrail = const []})
+      : _factorTrail = factorTrail;
+
+  factory _$SnAuthTicketImpl.fromJson(Map<String, dynamic> json) =>
+      _$$SnAuthTicketImplFromJson(json);
+
+  @override
+  final int id;
+  @override
+  final DateTime createdAt;
+  @override
+  final DateTime updatedAt;
+  @override
+  final DateTime? deletedAt;
+  @override
+  final int stepRemain;
+  @override
+  final String? grantToken;
+  @override
+  final String? accessToken;
+  @override
+  final String? refreshToken;
+  @override
+  final String ipAddress;
+  @override
+  final String location;
+  @override
+  final String userAgent;
+  @override
+  final DateTime? expiredAt;
+  @override
+  final DateTime? lastGrantAt;
+  @override
+  final DateTime? availableAt;
+  @override
+  final String? nonce;
+  @override
+  final int? accountId;
+  final List<int> _factorTrail;
+  @override
+  @JsonKey()
+  List<int> get factorTrail {
+    if (_factorTrail is EqualUnmodifiableListView) return _factorTrail;
+    // ignore: implicit_dynamic_type
+    return EqualUnmodifiableListView(_factorTrail);
+  }
+
+  @override
+  String toString() {
+    return 'SnAuthTicket(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, stepRemain: $stepRemain, grantToken: $grantToken, accessToken: $accessToken, refreshToken: $refreshToken, ipAddress: $ipAddress, location: $location, userAgent: $userAgent, expiredAt: $expiredAt, lastGrantAt: $lastGrantAt, availableAt: $availableAt, nonce: $nonce, accountId: $accountId, factorTrail: $factorTrail)';
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$SnAuthTicketImpl &&
+            (identical(other.id, id) || other.id == id) &&
+            (identical(other.createdAt, createdAt) ||
+                other.createdAt == createdAt) &&
+            (identical(other.updatedAt, updatedAt) ||
+                other.updatedAt == updatedAt) &&
+            (identical(other.deletedAt, deletedAt) ||
+                other.deletedAt == deletedAt) &&
+            (identical(other.stepRemain, stepRemain) ||
+                other.stepRemain == stepRemain) &&
+            (identical(other.grantToken, grantToken) ||
+                other.grantToken == grantToken) &&
+            (identical(other.accessToken, accessToken) ||
+                other.accessToken == accessToken) &&
+            (identical(other.refreshToken, refreshToken) ||
+                other.refreshToken == refreshToken) &&
+            (identical(other.ipAddress, ipAddress) ||
+                other.ipAddress == ipAddress) &&
+            (identical(other.location, location) ||
+                other.location == location) &&
+            (identical(other.userAgent, userAgent) ||
+                other.userAgent == userAgent) &&
+            (identical(other.expiredAt, expiredAt) ||
+                other.expiredAt == expiredAt) &&
+            (identical(other.lastGrantAt, lastGrantAt) ||
+                other.lastGrantAt == lastGrantAt) &&
+            (identical(other.availableAt, availableAt) ||
+                other.availableAt == availableAt) &&
+            (identical(other.nonce, nonce) || other.nonce == nonce) &&
+            (identical(other.accountId, accountId) ||
+                other.accountId == accountId) &&
+            const DeepCollectionEquality()
+                .equals(other._factorTrail, _factorTrail));
+  }
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  int get hashCode => Object.hash(
+      runtimeType,
+      id,
+      createdAt,
+      updatedAt,
+      deletedAt,
+      stepRemain,
+      grantToken,
+      accessToken,
+      refreshToken,
+      ipAddress,
+      location,
+      userAgent,
+      expiredAt,
+      lastGrantAt,
+      availableAt,
+      nonce,
+      accountId,
+      const DeepCollectionEquality().hash(_factorTrail));
+
+  /// Create a copy of SnAuthTicket
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$SnAuthTicketImplCopyWith<_$SnAuthTicketImpl> get copyWith =>
+      __$$SnAuthTicketImplCopyWithImpl<_$SnAuthTicketImpl>(this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$SnAuthTicketImplToJson(
+      this,
+    );
+  }
+}
+
+abstract class _SnAuthTicket implements SnAuthTicket {
+  const factory _SnAuthTicket(
+      {required final int id,
+      required final DateTime createdAt,
+      required final DateTime updatedAt,
+      required final DateTime? deletedAt,
+      required final int stepRemain,
+      required final String? grantToken,
+      required final String? accessToken,
+      required final String? refreshToken,
+      required final String ipAddress,
+      required final String location,
+      required final String userAgent,
+      required final DateTime? expiredAt,
+      required final DateTime? lastGrantAt,
+      required final DateTime? availableAt,
+      required final String? nonce,
+      required final int? accountId,
+      final List<int> factorTrail}) = _$SnAuthTicketImpl;
+
+  factory _SnAuthTicket.fromJson(Map<String, dynamic> json) =
+      _$SnAuthTicketImpl.fromJson;
+
+  @override
+  int get id;
+  @override
+  DateTime get createdAt;
+  @override
+  DateTime get updatedAt;
+  @override
+  DateTime? get deletedAt;
+  @override
+  int get stepRemain;
+  @override
+  String? get grantToken;
+  @override
+  String? get accessToken;
+  @override
+  String? get refreshToken;
+  @override
+  String get ipAddress;
+  @override
+  String get location;
+  @override
+  String get userAgent;
+  @override
+  DateTime? get expiredAt;
+  @override
+  DateTime? get lastGrantAt;
+  @override
+  DateTime? get availableAt;
+  @override
+  String? get nonce;
+  @override
+  int? get accountId;
+  @override
+  List<int> get factorTrail;
+
+  /// Create a copy of SnAuthTicket
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  _$$SnAuthTicketImplCopyWith<_$SnAuthTicketImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+SnAuthFactor _$SnAuthFactorFromJson(Map<String, dynamic> json) {
+  return _SnAuthFactor.fromJson(json);
+}
+
+/// @nodoc
+mixin _$SnAuthFactor {
+  int get id => throw _privateConstructorUsedError;
+  DateTime get createdAt => throw _privateConstructorUsedError;
+  DateTime get updatedAt => throw _privateConstructorUsedError;
+  DateTime? get deletedAt => throw _privateConstructorUsedError;
+  int get type => throw _privateConstructorUsedError;
+  Map<String, dynamic>? get config => throw _privateConstructorUsedError;
+  int? get accountId => throw _privateConstructorUsedError;
+
+  /// Serializes this SnAuthFactor to a JSON map.
+  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
+
+  /// Create a copy of SnAuthFactor
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  $SnAuthFactorCopyWith<SnAuthFactor> get copyWith =>
+      throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $SnAuthFactorCopyWith<$Res> {
+  factory $SnAuthFactorCopyWith(
+          SnAuthFactor value, $Res Function(SnAuthFactor) then) =
+      _$SnAuthFactorCopyWithImpl<$Res, SnAuthFactor>;
+  @useResult
+  $Res call(
+      {int id,
+      DateTime createdAt,
+      DateTime updatedAt,
+      DateTime? deletedAt,
+      int type,
+      Map<String, dynamic>? config,
+      int? accountId});
+}
+
+/// @nodoc
+class _$SnAuthFactorCopyWithImpl<$Res, $Val extends SnAuthFactor>
+    implements $SnAuthFactorCopyWith<$Res> {
+  _$SnAuthFactorCopyWithImpl(this._value, this._then);
+
+  // ignore: unused_field
+  final $Val _value;
+  // ignore: unused_field
+  final $Res Function($Val) _then;
+
+  /// Create a copy of SnAuthFactor
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? id = null,
+    Object? createdAt = null,
+    Object? updatedAt = null,
+    Object? deletedAt = freezed,
+    Object? type = null,
+    Object? config = freezed,
+    Object? accountId = freezed,
+  }) {
+    return _then(_value.copyWith(
+      id: null == id
+          ? _value.id
+          : id // ignore: cast_nullable_to_non_nullable
+              as int,
+      createdAt: null == createdAt
+          ? _value.createdAt
+          : createdAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      updatedAt: null == updatedAt
+          ? _value.updatedAt
+          : updatedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      deletedAt: freezed == deletedAt
+          ? _value.deletedAt
+          : deletedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      type: null == type
+          ? _value.type
+          : type // ignore: cast_nullable_to_non_nullable
+              as int,
+      config: freezed == config
+          ? _value.config
+          : config // ignore: cast_nullable_to_non_nullable
+              as Map<String, dynamic>?,
+      accountId: freezed == accountId
+          ? _value.accountId
+          : accountId // ignore: cast_nullable_to_non_nullable
+              as int?,
+    ) as $Val);
+  }
+}
+
+/// @nodoc
+abstract class _$$SnAuthFactorImplCopyWith<$Res>
+    implements $SnAuthFactorCopyWith<$Res> {
+  factory _$$SnAuthFactorImplCopyWith(
+          _$SnAuthFactorImpl value, $Res Function(_$SnAuthFactorImpl) then) =
+      __$$SnAuthFactorImplCopyWithImpl<$Res>;
+  @override
+  @useResult
+  $Res call(
+      {int id,
+      DateTime createdAt,
+      DateTime updatedAt,
+      DateTime? deletedAt,
+      int type,
+      Map<String, dynamic>? config,
+      int? accountId});
+}
+
+/// @nodoc
+class __$$SnAuthFactorImplCopyWithImpl<$Res>
+    extends _$SnAuthFactorCopyWithImpl<$Res, _$SnAuthFactorImpl>
+    implements _$$SnAuthFactorImplCopyWith<$Res> {
+  __$$SnAuthFactorImplCopyWithImpl(
+      _$SnAuthFactorImpl _value, $Res Function(_$SnAuthFactorImpl) _then)
+      : super(_value, _then);
+
+  /// Create a copy of SnAuthFactor
+  /// with the given fields replaced by the non-null parameter values.
+  @pragma('vm:prefer-inline')
+  @override
+  $Res call({
+    Object? id = null,
+    Object? createdAt = null,
+    Object? updatedAt = null,
+    Object? deletedAt = freezed,
+    Object? type = null,
+    Object? config = freezed,
+    Object? accountId = freezed,
+  }) {
+    return _then(_$SnAuthFactorImpl(
+      id: null == id
+          ? _value.id
+          : id // ignore: cast_nullable_to_non_nullable
+              as int,
+      createdAt: null == createdAt
+          ? _value.createdAt
+          : createdAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      updatedAt: null == updatedAt
+          ? _value.updatedAt
+          : updatedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime,
+      deletedAt: freezed == deletedAt
+          ? _value.deletedAt
+          : deletedAt // ignore: cast_nullable_to_non_nullable
+              as DateTime?,
+      type: null == type
+          ? _value.type
+          : type // ignore: cast_nullable_to_non_nullable
+              as int,
+      config: freezed == config
+          ? _value._config
+          : config // ignore: cast_nullable_to_non_nullable
+              as Map<String, dynamic>?,
+      accountId: freezed == accountId
+          ? _value.accountId
+          : accountId // ignore: cast_nullable_to_non_nullable
+              as int?,
+    ));
+  }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$SnAuthFactorImpl implements _SnAuthFactor {
+  const _$SnAuthFactorImpl(
+      {required this.id,
+      required this.createdAt,
+      required this.updatedAt,
+      required this.deletedAt,
+      required this.type,
+      required final Map<String, dynamic>? config,
+      required this.accountId})
+      : _config = config;
+
+  factory _$SnAuthFactorImpl.fromJson(Map<String, dynamic> json) =>
+      _$$SnAuthFactorImplFromJson(json);
+
+  @override
+  final int id;
+  @override
+  final DateTime createdAt;
+  @override
+  final DateTime updatedAt;
+  @override
+  final DateTime? deletedAt;
+  @override
+  final int type;
+  final Map<String, dynamic>? _config;
+  @override
+  Map<String, dynamic>? get config {
+    final value = _config;
+    if (value == null) return null;
+    if (_config is EqualUnmodifiableMapView) return _config;
+    // ignore: implicit_dynamic_type
+    return EqualUnmodifiableMapView(value);
+  }
+
+  @override
+  final int? accountId;
+
+  @override
+  String toString() {
+    return 'SnAuthFactor(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, config: $config, accountId: $accountId)';
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return identical(this, other) ||
+        (other.runtimeType == runtimeType &&
+            other is _$SnAuthFactorImpl &&
+            (identical(other.id, id) || other.id == id) &&
+            (identical(other.createdAt, createdAt) ||
+                other.createdAt == createdAt) &&
+            (identical(other.updatedAt, updatedAt) ||
+                other.updatedAt == updatedAt) &&
+            (identical(other.deletedAt, deletedAt) ||
+                other.deletedAt == deletedAt) &&
+            (identical(other.type, type) || other.type == type) &&
+            const DeepCollectionEquality().equals(other._config, _config) &&
+            (identical(other.accountId, accountId) ||
+                other.accountId == accountId));
+  }
+
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
+      deletedAt, type, const DeepCollectionEquality().hash(_config), accountId);
+
+  /// Create a copy of SnAuthFactor
+  /// with the given fields replaced by the non-null parameter values.
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  @override
+  @pragma('vm:prefer-inline')
+  _$$SnAuthFactorImplCopyWith<_$SnAuthFactorImpl> get copyWith =>
+      __$$SnAuthFactorImplCopyWithImpl<_$SnAuthFactorImpl>(this, _$identity);
+
+  @override
+  Map<String, dynamic> toJson() {
+    return _$$SnAuthFactorImplToJson(
+      this,
+    );
+  }
+}
+
+abstract class _SnAuthFactor implements SnAuthFactor {
+  const factory _SnAuthFactor(
+      {required final int id,
+      required final DateTime createdAt,
+      required final DateTime updatedAt,
+      required final DateTime? deletedAt,
+      required final int type,
+      required final Map<String, dynamic>? config,
+      required final int? accountId}) = _$SnAuthFactorImpl;
+
+  factory _SnAuthFactor.fromJson(Map<String, dynamic> json) =
+      _$SnAuthFactorImpl.fromJson;
+
+  @override
+  int get id;
+  @override
+  DateTime get createdAt;
+  @override
+  DateTime get updatedAt;
+  @override
+  DateTime? get deletedAt;
+  @override
+  int get type;
+  @override
+  Map<String, dynamic>? get config;
+  @override
+  int? get accountId;
+
+  /// Create a copy of SnAuthFactor
+  /// with the given fields replaced by the non-null parameter values.
+  @override
+  @JsonKey(includeFromJson: false, includeToJson: false)
+  _$$SnAuthFactorImplCopyWith<_$SnAuthFactorImpl> get copyWith =>
+      throw _privateConstructorUsedError;
+}
diff --git a/lib/types/auth.g.dart b/lib/types/auth.g.dart
new file mode 100644
index 0000000..a8a8a38
--- /dev/null
+++ b/lib/types/auth.g.dart
@@ -0,0 +1,98 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'auth.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$SnAuthResultImpl _$$SnAuthResultImplFromJson(Map<String, dynamic> json) =>
+    _$SnAuthResultImpl(
+      isFinished: json['is_finished'] as bool,
+      ticket: json['ticket'] == null
+          ? null
+          : SnAuthTicket.fromJson(json['ticket'] as Map<String, dynamic>),
+    );
+
+Map<String, dynamic> _$$SnAuthResultImplToJson(_$SnAuthResultImpl instance) =>
+    <String, dynamic>{
+      'is_finished': instance.isFinished,
+      'ticket': instance.ticket?.toJson(),
+    };
+
+_$SnAuthTicketImpl _$$SnAuthTicketImplFromJson(Map<String, dynamic> json) =>
+    _$SnAuthTicketImpl(
+      id: (json['id'] as num).toInt(),
+      createdAt: DateTime.parse(json['created_at'] as String),
+      updatedAt: DateTime.parse(json['updated_at'] as String),
+      deletedAt: json['deleted_at'] == null
+          ? null
+          : DateTime.parse(json['deleted_at'] as String),
+      stepRemain: (json['step_remain'] as num).toInt(),
+      grantToken: json['grant_token'] as String?,
+      accessToken: json['access_token'] as String?,
+      refreshToken: json['refresh_token'] as String?,
+      ipAddress: json['ip_address'] as String,
+      location: json['location'] as String,
+      userAgent: json['user_agent'] as String,
+      expiredAt: json['expired_at'] == null
+          ? null
+          : DateTime.parse(json['expired_at'] as String),
+      lastGrantAt: json['last_grant_at'] == null
+          ? null
+          : DateTime.parse(json['last_grant_at'] as String),
+      availableAt: json['available_at'] == null
+          ? null
+          : DateTime.parse(json['available_at'] as String),
+      nonce: json['nonce'] as String?,
+      accountId: (json['account_id'] as num?)?.toInt(),
+      factorTrail: (json['factor_trail'] as List<dynamic>?)
+              ?.map((e) => (e as num).toInt())
+              .toList() ??
+          const [],
+    );
+
+Map<String, dynamic> _$$SnAuthTicketImplToJson(_$SnAuthTicketImpl instance) =>
+    <String, dynamic>{
+      'id': instance.id,
+      'created_at': instance.createdAt.toIso8601String(),
+      'updated_at': instance.updatedAt.toIso8601String(),
+      'deleted_at': instance.deletedAt?.toIso8601String(),
+      'step_remain': instance.stepRemain,
+      'grant_token': instance.grantToken,
+      'access_token': instance.accessToken,
+      'refresh_token': instance.refreshToken,
+      'ip_address': instance.ipAddress,
+      'location': instance.location,
+      'user_agent': instance.userAgent,
+      'expired_at': instance.expiredAt?.toIso8601String(),
+      'last_grant_at': instance.lastGrantAt?.toIso8601String(),
+      'available_at': instance.availableAt?.toIso8601String(),
+      'nonce': instance.nonce,
+      'account_id': instance.accountId,
+      'factor_trail': instance.factorTrail,
+    };
+
+_$SnAuthFactorImpl _$$SnAuthFactorImplFromJson(Map<String, dynamic> json) =>
+    _$SnAuthFactorImpl(
+      id: (json['id'] as num).toInt(),
+      createdAt: DateTime.parse(json['created_at'] as String),
+      updatedAt: DateTime.parse(json['updated_at'] as String),
+      deletedAt: json['deleted_at'] == null
+          ? null
+          : DateTime.parse(json['deleted_at'] as String),
+      type: (json['type'] as num).toInt(),
+      config: json['config'] as Map<String, dynamic>?,
+      accountId: (json['account_id'] as num?)?.toInt(),
+    );
+
+Map<String, dynamic> _$$SnAuthFactorImplToJson(_$SnAuthFactorImpl instance) =>
+    <String, dynamic>{
+      'id': instance.id,
+      'created_at': instance.createdAt.toIso8601String(),
+      'updated_at': instance.updatedAt.toIso8601String(),
+      'deleted_at': instance.deletedAt?.toIso8601String(),
+      'type': instance.type,
+      'config': instance.config,
+      'account_id': instance.accountId,
+    };
diff --git a/lib/widgets/dialog.dart b/lib/widgets/dialog.dart
new file mode 100644
index 0000000..c5fdb2d
--- /dev/null
+++ b/lib/widgets/dialog.dart
@@ -0,0 +1,151 @@
+import 'dart:math' as math;
+
+import 'package:dio/dio.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+
+extension AppPromptExtension on BuildContext {
+  void showSnackbar(String content, {SnackBarAction? action}) {
+    ScaffoldMessenger.of(this).showSnackBar(SnackBar(
+      content: Text(content),
+      action: action,
+    ));
+  }
+
+  void clearSnackbar() {
+    ScaffoldMessenger.of(this).clearSnackBars();
+  }
+
+  Future<void> showModalDialog(String title, desc) {
+    return showDialog<void>(
+      useRootNavigator: true,
+      context: this,
+      builder: (ctx) => AlertDialog(
+        title: Text(title),
+        content: Text(desc),
+        actions: [
+          TextButton(
+            onPressed: () => Navigator.pop(ctx),
+            child: Text('dialogDismiss').tr(),
+          )
+        ],
+      ),
+    );
+  }
+
+  Future<void> showInfoDialog(String title, body) {
+    return showDialog<void>(
+      useRootNavigator: true,
+      context: this,
+      builder: (ctx) => AlertDialog(
+        title: Text(title),
+        content: Text(body),
+        actions: [
+          TextButton(
+            onPressed: () => Navigator.pop(ctx),
+            child: Text('dialogDismiss').tr(),
+          )
+        ],
+      ),
+    );
+  }
+
+  Future<bool> showConfirmDialog(String title, body) async {
+    return await showDialog<bool>(
+          useRootNavigator: true,
+          context: this,
+          builder: (ctx) => AlertDialog(
+            title: Text(title),
+            content: Text(body),
+            actions: [
+              TextButton(
+                onPressed: () => Navigator.pop(ctx, false),
+                child: Text('dialogCancel').tr(),
+              ),
+              TextButton(
+                onPressed: () => Navigator.pop(ctx, true),
+                child: Text('dialogConfirm').tr(),
+              )
+            ],
+          ),
+        ) ??
+        false;
+  }
+
+  Future<void> showErrorDialog(dynamic exception) {
+    Widget content = Text(exception.toString().capitalize());
+    if (exception is DioException) {
+      String preview;
+      switch (exception.response?.statusCode) {
+        case 400:
+          preview = 'errorRequestBad'.tr();
+          break;
+        case 401:
+          preview = 'errorRequestUnauthorized'.tr();
+          break;
+        case 403:
+          preview = 'errorRequestForbidden'.tr();
+          break;
+        case 404:
+          preview = 'errorRequestNotFound'.tr();
+          break;
+        case null:
+          preview = 'errorRequestConnection'.tr();
+          break;
+        default:
+          preview = 'errorRequestUnknown'.tr();
+          break;
+      }
+
+      if (exception.response != null) {
+        content = Text(
+          '$preview\n\n(${exception.response?.statusCode}) ${exception.response?.data}',
+        );
+      } else {
+        content = Text(preview);
+      }
+    }
+
+    return showDialog<void>(
+      useRootNavigator: true,
+      context: this,
+      builder: (ctx) => AlertDialog(
+        title: Text('dialogError').tr(),
+        content: content,
+        actions: [
+          TextButton(
+            onPressed: () => Navigator.pop(ctx),
+            child: Text('dialogDismiss').tr(),
+          )
+        ],
+      ),
+    );
+  }
+}
+
+extension ByteFormatter on int {
+  String formatBytes({int decimals = 2}) {
+    if (this == 0) return '0 Bytes';
+    const k = 1024;
+    final dm = decimals < 0 ? 0 : decimals;
+    final sizes = [
+      'Bytes',
+      'KiB',
+      'MiB',
+      'GiB',
+      'TiB',
+      'PiB',
+      'EiB',
+      'ZiB',
+      'YiB'
+    ];
+    final i = (math.log(this) / math.log(k)).floor().toInt();
+    return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
+  }
+}
+
+extension StringFormatter on String {
+  String capitalize() {
+    return "${this[0].toUpperCase()}${substring(1)}";
+  }
+}
diff --git a/lib/widgets/navigation/app_scaffold.dart b/lib/widgets/navigation/app_scaffold.dart
index a2dfd32..5b7f34f 100644
--- a/lib/widgets/navigation/app_scaffold.dart
+++ b/lib/widgets/navigation/app_scaffold.dart
@@ -1,24 +1,48 @@
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
 import 'package:responsive_framework/responsive_framework.dart';
+import 'package:surface/widgets/dialog.dart';
 import 'package:surface/widgets/navigation/app_background.dart';
 import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
 
 class AppScaffold extends StatelessWidget {
   final PreferredSizeWidget? appBar;
+  final String? title;
   final Widget? body;
-  final bool? showBottomNavigation;
-  const AppScaffold(
-      {super.key, this.appBar, this.body, this.showBottomNavigation});
+  final bool autoImplyAppBar;
+  final bool showBottomNavigation;
+  const AppScaffold({
+    super.key,
+    this.appBar,
+    this.title,
+    this.body,
+    this.autoImplyAppBar = false,
+    this.showBottomNavigation = false,
+  });
 
   @override
   Widget build(BuildContext context) {
-    final isShowBottomNavigation = (showBottomNavigation ?? false)
+    final isShowBottomNavigation = (showBottomNavigation)
         ? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
         : false;
 
+    final state = GoRouter.maybeOf(context);
+
     return AppBackground(
       child: Scaffold(
-        appBar: appBar,
+        appBar: appBar ??
+            (autoImplyAppBar
+                ? AppBar(
+                    title: title != null
+                        ? Text(title!)
+                        : state != null
+                            ? Text(
+                                ('screen${state.routerDelegate.currentConfiguration.last.route.name?.capitalize()}')
+                                    .tr(),
+                              )
+                            : null)
+                : null),
         body: body,
         bottomNavigationBar:
             isShowBottomNavigation ? AppBottomNavigationBar() : null,
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index f6f23bf..80f0c37 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -6,9 +6,13 @@
 
 #include "generated_plugin_registrant.h"
 
+#include <flutter_secure_storage/flutter_secure_storage_plugin.h>
 #include <url_launcher_linux/url_launcher_plugin.h>
 
 void fl_register_plugins(FlPluginRegistry* registry) {
+  g_autoptr(FlPluginRegistrar) flutter_secure_storage_registrar =
+      fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStoragePlugin");
+  flutter_secure_storage_plugin_register_with_registrar(flutter_secure_storage_registrar);
   g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
       fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
   url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index df8d2f7..3d4708b 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -3,6 +3,7 @@
 #
 
 list(APPEND FLUTTER_PLUGIN_LIST
+  flutter_secure_storage
   url_launcher_linux
 )
 
diff --git a/pubspec.lock b/pubspec.lock
index 9636647..b028ed2 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -416,6 +416,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.4.2"
+  flutter_secure_storage:
+    dependency: "direct main"
+    description:
+      name: flutter_secure_storage
+      sha256: "9f3dd2ac3b6875b0fde5b04734789c3ef35ba3965c18e99dd564a7a2f8056df6"
+      url: "https://pub.dev"
+    source: hosted
+    version: "4.2.1"
   flutter_shaders:
     dependency: transitive
     description:
@@ -886,10 +894,10 @@ packages:
     dependency: transitive
     description:
       name: shared_preferences
-      sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
+      sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.2"
+    version: "2.3.3"
   shared_preferences_android:
     dependency: transitive
     description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 50d011b..9ae35c2 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -58,6 +58,7 @@ dependencies:
   google_fonts: ^6.2.1
   path: ^1.9.0
   relative_time: ^5.0.0
+  flutter_secure_storage: ^4.2.1
 
 dev_dependencies:
   flutter_test:
@@ -87,6 +88,7 @@ flutter:
 
   # To add assets to your application, add an assets section, like this:
   assets:
+    - assets/icon/icon.png
     - assets/translations/
 
   # An image asset can refer to one or more resolution-specific "variants", see