login feature

This commit is contained in:
Simon Ding
2024-07-12 10:06:26 +08:00
parent a172ca0361
commit 60c3c8822e
16 changed files with 751 additions and 16 deletions

View File

@@ -8,6 +8,12 @@ const (
SettingDownloadDir = "download_dir"
)
const (
SettingAuthEnabled = "auth_enbled"
SettingUsername = "auth_username"
SettingPassword = "auth_password"
)
const (
IndexerTorznabImpl = "torznab"
)

2
go.mod
View File

@@ -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

2
go.sum
View File

@@ -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=

View File

@@ -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
}

141
server/auth.go Normal file
View File

@@ -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
}

View File

@@ -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")

82
ui/lib/login_page.dart Normal file
View File

@@ -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<String?> _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: <Widget>[
Expanded(child: Divider()),
Padding(
padding: EdgeInsets.all(8.0),
child: Text("Authenticate"),
),
Expanded(child: Divider()),
],
),
],
);
}
}

View File

@@ -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(),
)
],
);

View File

@@ -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<NavigatorState> navigatorKey =
GlobalKey<NavigatorState>();
static String baseUrl() {
if (kReleaseMode) {
return "";
}
return "http://127.0.0.1:8080";
}
static Dio? dio1;
static Future<Dio> 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;
}
}

View File

@@ -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, AuthSetting>(
AuthSettingData.new);
class AuthSettingData extends AutoDisposeAsyncNotifier<AuthSetting> {
@override
FutureOr<AuthSetting> 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<void> 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<void> 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<String, dynamic> json) {
return AuthSetting(enable: json["enable"], user: json["user"]);
}
}

View File

@@ -12,7 +12,8 @@ class SeriesDetailData
extends AutoDisposeFamilyAsyncNotifier<SeriesDetails, String> {
@override
FutureOr<SeriesDetails> 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<String> 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,

View File

@@ -22,11 +22,11 @@ var storageSettingProvider =
StorageSettingData.new);
class EditSettingData extends FamilyAsyncNotifier<String, String> {
final dio = Dio();
String? key;
@override
FutureOr<String> 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<String, String> {
}
Future<void> 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<String, String> {
}
class IndexerSetting extends AsyncNotifier<List<Indexer>> {
final dio = Dio();
@override
FutureOr<List<Indexer>> 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<List<Indexer>> {
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<List<Indexer>> {
}
Future<void> 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<List<DownloadClient>> {
final dio = Dio();
@override
FutureOr<List<DownloadClient>> 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<List<DownloadClient>> {
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<List<DownloadClient>> {
}
Future<void> 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<List<Storage>> {
final dio = Dio();
@override
FutureOr<List<Storage>> 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<List<Storage>> {
}
Future<void> 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<List<Storage>> {
}
Future<void> 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) {

View File

@@ -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<TvSeries> favList = List.empty(growable: true);
for (var item in sp.data as List) {
@@ -39,7 +40,7 @@ class SearchPageData extends AutoDisposeAsyncNotifier<List<SearchResult>> {
}
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());

View File

@@ -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<SystemSettingsPage> {
Future<void>? _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<SystemSettingsPage> {
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<SystemSettingsPage> {
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],
),
],
);
}

View File

@@ -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"

View File

@@ -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: