From 60c3c8822efb6265b25bbf2624b0f4ee988c21b8 Mon Sep 17 00:00:00 2001 From: Simon Ding Date: Fri, 12 Jul 2024 10:06:26 +0800 Subject: [PATCH] login feature --- db/const.go | 6 + go.mod | 2 + go.sum | 2 + pkg/utils/utils.go | 17 +- server/auth.go | 141 ++++++++++++ server/server.go | 5 + ui/lib/login_page.dart | 82 +++++++ ui/lib/main.dart | 10 +- ui/lib/providers/APIs.dart | 38 ++++ ui/lib/providers/login.dart | 61 ++++++ ui/lib/providers/series_details.dart | 6 +- ui/lib/providers/settings.dart | 20 +- ui/lib/providers/welcome_data.dart | 5 +- ui/lib/system_settings.dart | 64 ++++++ ui/pubspec.lock | 306 ++++++++++++++++++++++++++- ui/pubspec.yaml | 2 + 16 files changed, 751 insertions(+), 16 deletions(-) create mode 100644 server/auth.go create mode 100644 ui/lib/login_page.dart create mode 100644 ui/lib/providers/login.dart diff --git a/db/const.go b/db/const.go index 71874a0..7e7fdd4 100644 --- a/db/const.go +++ b/db/const.go @@ -8,6 +8,12 @@ const ( SettingDownloadDir = "download_dir" ) +const ( + SettingAuthEnabled = "auth_enbled" + SettingUsername = "auth_username" + SettingPassword = "auth_password" +) + const ( IndexerTorznabImpl = "torznab" ) diff --git a/go.mod b/go.mod index 52f4110..9921a51 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,8 @@ require ( golang.org/x/net v0.25.0 ) +require github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + require ( ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43 // indirect github.com/agext/levenshtein v1.2.1 // indirect diff --git a/go.sum b/go.sum index b0d3fe9..6094f2e 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 6cb1944..9ea87a9 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -1,6 +1,10 @@ package utils -import "unicode" +import ( + "unicode" + + "golang.org/x/crypto/bcrypt" +) func isASCII(s string) bool { for _, c := range s { @@ -11,3 +15,14 @@ func isASCII(s string) bool { return true } +// HashPassword generates a bcrypt hash for the given password. +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + return string(bytes), err +} + +// VerifyPassword verifies if the given password matches the stored hash. +func VerifyPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} \ No newline at end of file diff --git a/server/auth.go b/server/auth.go new file mode 100644 index 0000000..c9ff8b0 --- /dev/null +++ b/server/auth.go @@ -0,0 +1,141 @@ +package server + +import ( + "net/http" + "polaris/db" + "polaris/log" + "polaris/pkg/utils" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "github.com/pkg/errors" +) + +func (s *Server) isAuthEnabled() bool { + authEnabled := s.db.GetSetting(db.SettingAuthEnabled) + return authEnabled == "true" +} + +func (s *Server) authModdleware(c *gin.Context) { + if !s.isAuthEnabled() { + c.Next() + return + } + + auth := c.GetHeader("Authorization") + if auth == "" { + c.AbortWithStatus(http.StatusForbidden) + return + } + auth = strings.TrimPrefix(auth, "Bearer ") + log.Infof("current token: %v", auth) + token, err := jwt.ParseWithClaims(auth, &jwt.RegisteredClaims{}, func(t *jwt.Token) (interface{}, error) { + return []byte(secretKey), nil + }) + if err != nil { + log.Errorf("parse token error: %v", err) + c.AbortWithStatus(http.StatusForbidden) + return + } + if !token.Valid { + log.Errorf("token is not valid: %v", auth) + c.AbortWithStatus(http.StatusForbidden) + return + } + claim := token.Claims.(*jwt.RegisteredClaims) + + if time.Until(claim.ExpiresAt.Time) <= 0 { + log.Infof("token is no longer valid: %s", auth) + c.AbortWithStatus(http.StatusForbidden) + return + } + c.Next() + +} + +type LoginIn struct { + User string `json:"user"` + Password string `json:"password"` +} + +const secretKey = "r1OF7nhpNjnYiGKtTLuKEVq7YznzT" + +func (s *Server) Login(c *gin.Context) (interface{}, error) { + var in LoginIn + + if err := c.ShouldBindJSON(&in); err != nil { + return nil, errors.Wrap(err, "bind json") + } + + if !s.isAuthEnabled() { + return nil, nil + } + + user := s.db.GetSetting(db.SettingUsername) + if user != in.User { + return nil, errors.New("login fail") + } + password := s.db.GetSetting(db.SettingPassword) + if !utils.VerifyPassword(in.Password, password) { + return nil, errors.New("login fail") + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ + Issuer: "system", + Subject: in.User, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + }) + sig, err := token.SignedString([]byte(secretKey)) + if err != nil { + return nil, errors.Wrap(err, "sign") + } + return gin.H{ + "token": sig, + }, nil +} + +type EnableAuthIn struct { + Enable bool `json:"enable"` + User string `json:"user"` + Password string `json:"password"` +} + +func (s *Server) EnableAuth(c *gin.Context) (interface{}, error) { + var in EnableAuthIn + if err := c.ShouldBindJSON(&in); err != nil { + return nil, errors.Wrap(err, "bind json") + } + + if in.Enable && (in.User == "" || in.Password == "") { + return nil, errors.New("user password should not empty") + } + if !in.Enable { + log.Infof("disable auth") + s.db.SetSetting(db.SettingAuthEnabled, "false") + } else { + log.Info("enable auth") + s.db.SetSetting(db.SettingAuthEnabled, "true") + s.db.SetSetting(db.SettingUsername, in.User) + + hash, err := utils.HashPassword(in.Password) + if err != nil { + return nil, errors.Wrap(err, "hash password") + } + s.db.SetSetting(db.SettingPassword, hash) + } + return "success", nil +} + +func (s *Server) GetAuthSetting(c *gin.Context) (interface{}, error) { + enabled := s.db.GetSetting(db.SettingAuthEnabled) + user := s.db.GetSetting(db.SettingUsername) + + return EnableAuthIn{ + Enable: enabled == "true", + User: user, + }, nil +} diff --git a/server/server.go b/server/server.go index ae5a236..1d16fae 100644 --- a/server/server.go +++ b/server/server.go @@ -38,12 +38,17 @@ func (s *Server) Serve() error { //st, _ := fs.Sub(ui.Web, "build/web") s.r.Use(static.Serve("/", static.EmbedFolder(ui.Web, "build/web"))) + s.r.POST("/api/login", HttpHandler(s.Login)) + api := s.r.Group("/api/v1") + api.Use(s.authModdleware) setting := api.Group("/setting") { setting.POST("/do", HttpHandler(s.SetSetting)) setting.GET("/do", HttpHandler(s.GetSetting)) + setting.POST("/auth", HttpHandler(s.EnableAuth)) + setting.GET("/auth", HttpHandler(s.GetAuthSetting)) } tv := api.Group("/tv") diff --git a/ui/lib/login_page.dart b/ui/lib/login_page.dart new file mode 100644 index 0000000..6a75b13 --- /dev/null +++ b/ui/lib/login_page.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart' show timeDilation; +import 'package:flutter_login/flutter_login.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:quiver/strings.dart'; +import 'package:ui/providers/login.dart'; +import 'package:ui/weclome.dart'; + +class LoginScreen extends ConsumerWidget { + static const route = '/login'; + + const LoginScreen({super.key}); + + Duration get loginTime => Duration(milliseconds: timeDilation.ceil() * 2250); + + + Future _recoverPassword(String name) { + return Future.delayed(loginTime).then((_) { + return null; + }); + } + + + @override + Widget build(BuildContext context, WidgetRef ref) { + return FlutterLogin( + title: 'Polaris', + onLogin: (data) { + ref.read(authSettingProvider.notifier).login(data.name, data.password); + }, + onSubmitAnimationCompleted: () { + context.go(WelcomePage.route); + }, + onRecoverPassword: _recoverPassword, + userValidator: (value) => isBlank(value)? "不能为空":null, + userType: LoginUserType.name, + hideForgotPasswordButton: true, + messages: LoginMessages( + userHint: '用户名', + passwordHint: '密码', + loginButton: '登录', + ), + ); + } +} + +class IntroWidget extends StatelessWidget { + const IntroWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + Text.rich( + TextSpan( + children: [ + TextSpan( + text: "You are trying to login/sign up on server hosted on ", + ), + TextSpan( + text: "example.com", + style: TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + textAlign: TextAlign.justify, + ), + Row( + children: [ + Expanded(child: Divider()), + Padding( + padding: EdgeInsets.all(8.0), + child: Text("Authenticate"), + ), + Expanded(child: Divider()), + ], + ), + ], + ); + } +} diff --git a/ui/lib/main.dart b/ui/lib/main.dart index 0314d7c..7fdf953 100644 --- a/ui/lib/main.dart +++ b/ui/lib/main.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:ui/login_page.dart'; import 'package:ui/navdrawer.dart'; +import 'package:ui/providers/APIs.dart'; import 'package:ui/search.dart'; import 'package:ui/system_settings.dart'; import 'package:ui/tv_details.dart'; @@ -74,15 +76,19 @@ class MyApp extends StatelessWidget { path: TvDetailsPage.route, builder: (context, state) => TvDetailsPage(seriesId: state.pathParameters['id']!), - ) + ), ], ); final _router = GoRouter( - navigatorKey: _rootNavigatorKey, + navigatorKey: APIs.navigatorKey, initialLocation: WelcomePage.route, routes: [ _shellRoute, + GoRoute( + path: LoginScreen.route, + builder: (context, state) =>const LoginScreen(), + ) ], ); diff --git a/ui/lib/providers/APIs.dart b/ui/lib/providers/APIs.dart index 584d74f..74b3bba 100644 --- a/ui/lib/providers/APIs.dart +++ b/ui/lib/providers/APIs.dart @@ -1,4 +1,8 @@ +import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class APIs { static final _baseUrl = baseUrl(); @@ -14,16 +18,50 @@ class APIs { static final addDownloadClientUrl = "$_baseUrl/api/v1/downloader/add"; static final delDownloadClientUrl = "$_baseUrl/api/v1/downloader/del/"; static final storageUrl = "$_baseUrl/api/v1/storage/"; + static final loginUrl = "$_baseUrl/api/login"; + static final loginSettingUrl = "$_baseUrl/api/v1/setting/auth"; static const tmdbImgBaseUrl = "https://image.tmdb.org/t/p/w500/"; static const tmdbApiKey = "tmdb_api_key"; static const downloadDirKey = "download_dir"; + static final GlobalKey navigatorKey = + GlobalKey(); + static String baseUrl() { if (kReleaseMode) { return ""; } return "http://127.0.0.1:8080"; } + + static Dio? dio1; + + static Future getDio() async { + if (dio1 != null) { + return dio1!; + } + final SharedPreferences prefs = await SharedPreferences.getInstance(); + var token = prefs.getString("token"); + var dio = Dio(); + dio.interceptors.add(InterceptorsWrapper( + onRequest: (options, handler) { + options.headers['Authorization'] = "Bearer $token"; + return handler.next(options); + }, + onError: (error, handler) { + if (error.response?.statusCode != null && + error.response?.statusCode! == 403) { + final context = navigatorKey.currentContext; + if (context != null) { + context.go('/login'); + } + } + return handler.next(error); + }, + )); + dio1 = dio; + return dio; + } } diff --git a/ui/lib/providers/login.dart b/ui/lib/providers/login.dart new file mode 100644 index 0000000..3e0530b --- /dev/null +++ b/ui/lib/providers/login.dart @@ -0,0 +1,61 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:ui/providers/APIs.dart'; +import 'package:ui/providers/server_response.dart'; + +var authSettingProvider = + AsyncNotifierProvider.autoDispose( + AuthSettingData.new); + +class AuthSettingData extends AutoDisposeAsyncNotifier { + @override + FutureOr build() async { + final dio = await APIs.getDio(); + var resp = await dio.get(APIs.loginSettingUrl); + var sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + var as = AuthSetting.fromJson(sp.data); + return as; + } + + Future updateAuthSetting( + bool enable, String user, String password) async { + final dio = await APIs.getDio(); + var resp = await dio.post(APIs.loginSettingUrl, + data: {"enable": enable, "user": user, "password": password}); + var sp = ServerResponse.fromJson(resp.data); + if (sp.code != 0) { + throw sp.message; + } + ref.invalidateSelf(); + } + + Future login(String user, String password) async { + var resp = await Dio() + .post(APIs.loginUrl, data: {"user": user, "password": password}); + + var sp = ServerResponse.fromJson(resp.data); + + if (sp.code != 0) { + throw sp.message; + } + final SharedPreferences prefs = await SharedPreferences.getInstance(); + prefs.setString("token", sp.data["token"]); + } +} + +class AuthSetting { + bool enable; + String user; + + AuthSetting({required this.enable, required this.user}); + + factory AuthSetting.fromJson(Map json) { + return AuthSetting(enable: json["enable"], user: json["user"]); + } +} diff --git a/ui/lib/providers/series_details.dart b/ui/lib/providers/series_details.dart index 8fe20d7..69f6d7a 100644 --- a/ui/lib/providers/series_details.dart +++ b/ui/lib/providers/series_details.dart @@ -12,7 +12,8 @@ class SeriesDetailData extends AutoDisposeFamilyAsyncNotifier { @override FutureOr build(String arg) async { - var resp = await Dio().get("${APIs.seriesDetailUrl}$arg"); + final dio = await APIs.getDio(); + var resp = await dio.get("${APIs.seriesDetailUrl}$arg"); var rsp = ServerResponse.fromJson(resp.data); if (rsp.code != 0) { throw rsp.message; @@ -22,7 +23,8 @@ class SeriesDetailData Future searchAndDownload( String seriesId, int seasonNum, int episodeNum) async { - var resp = await Dio().post(APIs.searchAndDownloadUrl, data: { + final dio = await APIs.getDio(); + var resp = await dio.post(APIs.searchAndDownloadUrl, data: { "id": int.parse(seriesId), "season": seasonNum, "episode": episodeNum, diff --git a/ui/lib/providers/settings.dart b/ui/lib/providers/settings.dart index 5ab94e3..b76e5cc 100644 --- a/ui/lib/providers/settings.dart +++ b/ui/lib/providers/settings.dart @@ -22,11 +22,11 @@ var storageSettingProvider = StorageSettingData.new); class EditSettingData extends FamilyAsyncNotifier { - final dio = Dio(); String? key; @override FutureOr build(String arg) async { + final dio = await APIs.getDio(); key = arg; var resp = await dio.get(APIs.settingsUrl, queryParameters: {"key": arg}); var rrr = ServerResponse.fromJson(resp.data); @@ -40,6 +40,7 @@ class EditSettingData extends FamilyAsyncNotifier { } Future updateSettings(String v) async { + final dio = await APIs.getDio(); var resp = await dio.post(APIs.settingsUrl, data: { "key": key, "value": v, @@ -53,10 +54,9 @@ class EditSettingData extends FamilyAsyncNotifier { } class IndexerSetting extends AsyncNotifier> { - final dio = Dio(); - @override FutureOr> build() async { + final dio = await APIs.getDio(); var resp = await dio.get(APIs.allIndexersUrl); var sp = ServerResponse.fromJson(resp.data); if (sp.code != 0) { @@ -75,6 +75,7 @@ class IndexerSetting extends AsyncNotifier> { isBlank(indexer.apiKey)) { return; } + final dio = await APIs.getDio(); var resp = await dio.post(APIs.addIndexerUrl, data: indexer.toJson()); var sp = ServerResponse.fromJson(resp.data); if (sp.code != 0) { @@ -84,6 +85,7 @@ class IndexerSetting extends AsyncNotifier> { } Future deleteIndexer(int id) async { + final dio = await APIs.getDio(); var resp = await dio.delete("${APIs.delIndexerUrl}$id"); var sp = ServerResponse.fromJson(resp.data); if (sp.code != 0) { @@ -117,10 +119,9 @@ class Indexer { } class DownloadClientSetting extends AsyncNotifier> { - final dio = Dio(); - @override FutureOr> build() async { + final dio = await APIs.getDio(); var resp = await dio.get(APIs.allDownloadClientsUrl); var sp = ServerResponse.fromJson(resp.data); if (sp.code != 0) { @@ -137,7 +138,7 @@ class DownloadClientSetting extends AsyncNotifier> { if (name.isEmpty || url.isEmpty) { return; } - var dio = Dio(); + final dio = await APIs.getDio(); var resp = await dio.post(APIs.addDownloadClientUrl, data: { "name": name, "url": url, @@ -150,7 +151,7 @@ class DownloadClientSetting extends AsyncNotifier> { } Future deleteDownloadClients(int id) async { - var dio = Dio(); + final dio = await APIs.getDio(); var resp = await dio.delete("${APIs.delDownloadClientUrl}$id"); var sp = ServerResponse.fromJson(resp.data); if (sp.code != 0) { @@ -202,9 +203,10 @@ class DownloadClient { } class StorageSettingData extends AsyncNotifier> { - final dio = Dio(); + @override FutureOr> build() async { + final dio = await APIs.getDio(); var resp = await dio.get(APIs.storageUrl); var sp = ServerResponse.fromJson(resp.data); if (sp.code != 0) { @@ -219,6 +221,7 @@ class StorageSettingData extends AsyncNotifier> { } Future deleteStorage(int id) async { + final dio = await APIs.getDio(); var resp = await dio.delete("${APIs.storageUrl}$id"); var sp = ServerResponse.fromJson(resp.data); if (sp.code != 0) { @@ -228,6 +231,7 @@ class StorageSettingData extends AsyncNotifier> { } Future addStorage(Storage s) async { + final dio = await APIs.getDio(); var resp = await dio.post(APIs.storageUrl, data: s.toJson()); var sp = ServerResponse.fromJson(resp.data); if (sp.code != 0) { diff --git a/ui/lib/providers/welcome_data.dart b/ui/lib/providers/welcome_data.dart index 80f7616..8540f7b 100644 --- a/ui/lib/providers/welcome_data.dart +++ b/ui/lib/providers/welcome_data.dart @@ -6,7 +6,8 @@ import 'package:ui/providers/APIs.dart'; import 'package:ui/providers/server_response.dart'; final welcomePageDataProvider = FutureProvider((ref) async { - var resp = await Dio().get(APIs.watchlistUrl); + final dio = await APIs.getDio(); + var resp = await dio.get(APIs.watchlistUrl); var sp = ServerResponse.fromJson(resp.data); List favList = List.empty(growable: true); for (var item in sp.data as List) { @@ -39,7 +40,7 @@ class SearchPageData extends AutoDisposeAsyncNotifier> { } void queryResults(String q) async { - final dio = Dio(); + final dio = await APIs.getDio(); var resp = await dio.get(APIs.searchUrl, queryParameters: {"query": q}); //var dy = jsonDecode(resp.data.toString()); diff --git a/ui/lib/system_settings.dart b/ui/lib/system_settings.dart index a66f629..243f45f 100644 --- a/ui/lib/system_settings.dart +++ b/ui/lib/system_settings.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quiver/strings.dart'; +import 'package:ui/providers/login.dart'; import 'package:ui/providers/settings.dart'; import 'package:ui/utils.dart'; @@ -24,6 +25,8 @@ class _SystemSettingsPageState extends ConsumerState { Future? _pendingStorage; final _tmdbApiController = TextEditingController(); final _downloadDirController = TextEditingController(); + bool? _enableAuth; + @override Widget build(BuildContext context) { var tmdbKey = ref.watch(settingProvider(APIs.tmdbApiKey)); @@ -238,6 +241,60 @@ class _SystemSettingsPageState extends ConsumerState { loading: () => const CircularProgressIndicator()); }); + var authData = ref.watch(authSettingProvider); + TextEditingController _userController = TextEditingController(); + TextEditingController _passController = TextEditingController(); + var authSetting = authData.when( + data: (data) { + if (_enableAuth == null) { + setState(() { + _enableAuth = data.enable; + }); + } + _userController.text = data.user; + return Column( + children: [ + SwitchListTile( + title: const Text("开启认证"), + value: _enableAuth!, + onChanged: (v) { + setState(() { + _enableAuth = v; + }); + }), + _enableAuth! + ? Column( + children: [ + TextFormField( + controller: _userController, + decoration: const InputDecoration( + labelText: "用户名", + icon: Icon(Icons.verified_user), + )), + TextFormField( + controller: _passController, + decoration: const InputDecoration( + labelText: "密码", + icon: Icon(Icons.verified_user), + )) + ], + ) + : const Column(), + Center( + child: ElevatedButton( + child: const Text("保存"), + onPressed: () { + ref + .read(authSettingProvider.notifier) + .updateAuthSetting(_enableAuth!, + _userController.text, _passController.text); + })) + ], + ); + }, + error: (err, trace) => Text("$err"), + loading: () => const CircularProgressIndicator()); + return ListView( children: [ ExpansionTile( @@ -268,6 +325,13 @@ class _SystemSettingsPageState extends ConsumerState { title: const Text("存储设置"), children: [storageSetting], ), + ExpansionTile( + tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0), + initiallyExpanded: true, + title: const Text("认证设置"), + children: [authSetting], + ), ], ); } diff --git a/ui/pubspec.lock b/ui/pubspec.lock index 6f338b0..57e58cc 100644 --- a/ui/pubspec.lock +++ b/ui/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + another_flushbar: + dependency: transitive + description: + name: another_flushbar + sha256: "19bf9520230ec40b300aaf9dd2a8fefcb277b25ecd1c4838f530566965befc2a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.12.30" + another_transformer_page_view: + dependency: transitive + description: + name: another_transformer_page_view + sha256: a7cd46ede62d621c5abe7e58c7cb2745abe67b3bfec64f59b8889c93d7be7a8e + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" async: dependency: transitive description: @@ -57,6 +73,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "5.4.3+1" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -65,6 +89,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.0" flutter: dependency: "direct main" description: flutter @@ -78,6 +118,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.0.0" + flutter_login: + dependency: "direct main" + description: + name: flutter_login + sha256: "1f7c46d0d76081cf4c5180e3a265b1f5b1d7e48c81859f58f03a8dcd27338b85" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.0" flutter_riverpod: dependency: "direct main" description: @@ -96,6 +144,14 @@ packages: description: flutter source: sdk version: "0.0.0" + font_awesome_flutter: + dependency: transitive + description: + name: font_awesome_flutter + sha256: "275ff26905134bcb59417cf60ad979136f1f8257f2f449914b2c3e05bbb4cd6f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.7.0" go_router: dependency: "direct main" description: @@ -112,6 +168,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.0.2" + intl_phone_number_input: + dependency: transitive + description: + name: intl_phone_number_input + sha256: "1c4328713a9503ab26a1fdbb6b00b4cada68c18aac922b35bedbc72eff1297c3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.7" leak_tracker: dependency: transitive description: @@ -136,6 +208,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" + libphonenumber_platform_interface: + dependency: transitive + description: + name: libphonenumber_platform_interface + sha256: f801f6c65523f56504b83f0890e6dad584ab3a7507dca65fec0eed640afea40f + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.2" + libphonenumber_plugin: + dependency: transitive + description: + name: libphonenumber_plugin + sha256: c615021d9816fbda2b2587881019ed595ecdf54d999652d7e4cce0e1f026368c + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.3" + libphonenumber_web: + dependency: transitive + description: + name: libphonenumber_web + sha256: "8186f420dbe97c3132283e52819daff1e55d60d6db46f7ea5ac42f42a28cc2ef" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.2" lints: dependency: transitive description: @@ -176,6 +272,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.12.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" path: dependency: transitive description: @@ -184,6 +288,62 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.9.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + phone_numbers_parser: + dependency: transitive + description: + name: phone_numbers_parser + sha256: "62451b689d842791ed1fd5dc9eacf36ffa8bad23a78ad6cde732dc2fb222fae2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.5" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.8" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.2" quiver: dependency: "direct main" description: @@ -200,6 +360,70 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.5.1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.0" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + sign_in_button: + dependency: transitive + description: + name: sign_in_button + sha256: "977b9b0415d2f3909e642275dfabba7919ba8e111324641b76cae6d1acbd183e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.0" sky_engine: dependency: transitive description: flutter @@ -269,6 +493,70 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.3.2" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.0" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.3" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.1" vector_math: dependency: transitive description: @@ -285,6 +573,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "14.2.1" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.4" sdks: dart: ">=3.4.3 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.22.0" diff --git a/ui/pubspec.yaml b/ui/pubspec.yaml index e6e25d2..9a4561e 100644 --- a/ui/pubspec.yaml +++ b/ui/pubspec.yaml @@ -39,6 +39,8 @@ dependencies: go_router: ^14.2.0 flutter_riverpod: ^2.5.1 quiver: ^3.2.1 + flutter_login: ^5.0.0 + shared_preferences: ^2.2.3 dev_dependencies: flutter_test: