refactor: use drift db based authentication

This commit is contained in:
Kingkor Roy Tirtho 2024-06-16 22:33:23 +06:00
parent a799ca55bc
commit d18f74fd65
36 changed files with 1408 additions and 1004 deletions

View File

@ -32,7 +32,7 @@ import 'package:spotube/pages/stats/playlists/playlists.dart';
import 'package:spotube/pages/stats/stats.dart';
import 'package:spotube/pages/stats/streams/streams.dart';
import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/components/spotube_page_route.dart';
@ -59,11 +59,9 @@ final routerProvider = Provider((ref) {
path: "/",
name: HomePage.name,
redirect: (context, state) async {
final authNotifier = ref.read(authenticationProvider.notifier);
final json = await authNotifier.box.get(authNotifier.cacheKey);
final auth = await ref.read(authenticationProvider.future);
if (json?["cookie"] == null &&
!KVStoreService.doneGettingStarted) {
if (auth == null && !KVStoreService.doneGettingStarted) {
return "/getting-started";
}

View File

@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/utils/service_utils.dart';
class AnonymousFallback extends ConsumerWidget {

View File

@ -4,7 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/heart_button/use_track_toggle_like.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class HeartButton extends HookConsumerWidget {
@ -26,7 +26,7 @@ class HeartButton extends HookConsumerWidget {
Widget build(BuildContext context, ref) {
final auth = ref.watch(authenticationProvider);
if (auth == null) return const SizedBox.shrink();
if (auth.asData?.value == null) return const SizedBox.shrink();
return IconButton(
tooltip: tooltip,

View File

@ -20,7 +20,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';

View File

@ -9,7 +9,7 @@ import 'package:spotube/components/heart_button/heart_button.dart';
import 'package:spotube/components/tracks_view/sections/body/use_is_user_playlist.dart';
import 'package:spotube/components/tracks_view/track_view_props.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';

View File

@ -2,7 +2,7 @@ import 'package:spotube/services/logger/logger.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
@ -18,7 +18,7 @@ void useEndlessPlayback(WidgetRef ref) {
useEffect(
() {
if (!endlessPlayback || auth == null) return null;
if (!endlessPlayback || auth.asData?.value == null) return null;
void listener(int index) async {
try {

View File

@ -19,10 +19,11 @@ import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
part 'database.g.dart';
part 'tables/preferences.dart';
part 'tables/source_match.dart';
part 'tables/skip_segment.dart';
part 'tables/authentication.dart';
part 'tables/blacklist.dart';
part 'tables/preferences.dart';
part 'tables/skip_segment.dart';
part 'tables/source_match.dart';
part 'typeconverters/color.dart';
part 'typeconverters/locale.dart';
@ -31,10 +32,11 @@ part 'typeconverters/encrypted_text.dart';
@DriftDatabase(
tables: [
PreferencesTable,
SourceMatchTable,
SkipSegmentTable,
AuthenticationTable,
BlacklistTable,
PreferencesTable,
SkipSegmentTable,
SourceMatchTable,
],
)
class AppDatabase extends _$AppDatabase {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
part of '../database.dart';
class AuthenticationTable extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get cookie => text().map(EncryptedTextConverter())();
TextColumn get accessToken => text().map(EncryptedTextConverter())();
DateTimeColumn get expiration => dateTime()();
}

View File

@ -22,6 +22,11 @@ class DecryptedText {
}
String encrypt() {
_encrypter ??= Encrypter(
Salsa20(
Key.fromUtf8(EncryptedKvStoreService.encryptionKeySync),
),
);
return _encrypter!.encrypt(value, iv: KVStoreService.ivKey).base64;
}
}

View File

@ -5,7 +5,7 @@ class StringListConverter extends TypeConverter<List<String>, String> {
@override
List<String> fromSql(String fromDb) {
return fromDb.split(",");
return fromDb.split(",").where((e) => e.isNotEmpty).toList();
}
@override

View File

@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
class TokenLoginForm extends HookConsumerWidget {
final void Function()? onDone;
@ -52,10 +52,7 @@ class TokenLoginForm extends HookConsumerWidget {
final cookieHeader =
"sp_dc=${directCodeController.text.trim()}";
authenticationNotifier.setCredentials(
await AuthenticationCredentials.fromCookie(
cookieHeader),
);
await authenticationNotifier.login(cookieHeader);
if (context.mounted) {
onDone?.call();
}

View File

@ -8,7 +8,7 @@ import 'package:spotube/collections/fake.dart';
import 'package:spotube/modules/home/sections/friends/friend_item.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:spotube/models/spotify_friends.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class HomePageFriendsSection extends HookConsumerWidget {
@ -59,7 +59,7 @@ class HomePageFriendsSection extends HookConsumerWidget {
if (friendsQuery.isLoading ||
friendsQuery.asData?.value.friends.isEmpty == true ||
auth == null) {
auth.asData?.value == null) {
return const SliverToBoxAdapter(
child: SizedBox.shrink(),
);

View File

@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class HomeNewReleasesSection extends HookConsumerWidget {
@ -18,7 +18,7 @@ class HomeNewReleasesSection extends HookConsumerWidget {
final albums = ref.watch(userArtistAlbumReleasesProvider);
if (auth == null ||
if (auth.asData?.value == null ||
newReleases.isLoading ||
newReleases.asData?.value.items.isEmpty == true) {
return const SizedBox.shrink();

View File

@ -46,7 +46,7 @@ class LocalFolderItem extends HookConsumerWidget {
...pathSegments.skip(pathSegments.length - 3).toList()
..removeLast(),
]
: pathSegments.take(pathSegments.length - 1).toList();
: pathSegments.take(max(pathSegments.length - 1, 0)).toList();
final trackSnapshot = ref.watch(
localTracksProvider.select(

View File

@ -14,7 +14,7 @@ import 'package:spotube/components/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/waypoint.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class UserAlbums extends HookConsumerWidget {
@ -46,7 +46,7 @@ class UserAlbums extends HookConsumerWidget {
[];
}, [albumsQuery.asData?.value, searchText.value]);
if (auth == null) {
if (auth.asData?.value == null) {
return const AnonymousFallback();
}

View File

@ -14,7 +14,7 @@ import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/waypoint.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart';
class UserArtists extends HookConsumerWidget {
@ -48,7 +48,7 @@ class UserArtists extends HookConsumerWidget {
final controller = useScrollController();
if (auth == null) {
if (auth.asData?.value == null) {
return const AnonymousFallback();
}

View File

@ -17,7 +17,7 @@ import 'package:spotube/components/waypoint.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart';
@ -75,7 +75,7 @@ class UserPlaylists extends HookConsumerWidget {
final controller = useScrollController();
if (auth == null) {
if (auth.asData?.value == null) {
return const AnonymousFallback();
}

View File

@ -24,7 +24,7 @@ import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart';
import 'package:spotube/hooks/utils/use_palette_color.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/pages/lyrics/lyrics.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/server/active_sourced_track.dart';
import 'package:spotube/provider/volume_provider.dart';

View File

@ -13,7 +13,7 @@ import 'package:spotube/extensions/duration.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/sleep_timer_provider.dart';

View File

@ -18,7 +18,7 @@ import 'package:spotube/extensions/image.dart';
import 'package:spotube/hooks/utils/use_brightness_value.dart';
import 'package:spotube/models/logger.dart';
import 'package:flutter/material.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';

View File

@ -20,7 +20,7 @@ import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart';
import 'package:spotube/pages/profile/profile.dart';
import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
@ -269,7 +269,7 @@ class SidebarFooter extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (auth != null && data == null)
if (auth.asData?.value != null && data == null)
const CircularProgressIndicator()
else if (data != null)
Flexible(

View File

@ -11,7 +11,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/primitive_utils.dart';

View File

@ -9,7 +9,7 @@ import 'package:spotube/components/links/hyper_link.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/home/home.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/utils/service_utils.dart';
class LoginTutorial extends ConsumerWidget {
@ -18,8 +18,7 @@ class LoginTutorial extends ConsumerWidget {
@override
Widget build(BuildContext context, ref) {
ref.watch(authenticationProvider);
final authenticationNotifier = ref.watch(authenticationProvider.notifier);
final auth = ref.watch(authenticationProvider);
final key = GlobalKey<State<IntroductionScreen>>();
final theme = Theme.of(context);
@ -53,7 +52,7 @@ class LoginTutorial extends ConsumerWidget {
),
showBackButton: true,
overrideDone: FilledButton(
onPressed: authenticationNotifier.isLoggedIn
onPressed: auth.asData?.value != null
? () {
ServiceUtils.pushNamed(context, HomePage.name);
}
@ -91,7 +90,7 @@ class LoginTutorial extends ConsumerWidget {
bodyWidget:
Text(context.l10n.step_3_steps, textAlign: TextAlign.left),
),
if (authenticationNotifier.isLoggedIn)
if (auth.asData?.value != null)
PageViewModel(
decoration: pageDecoration.copyWith(
bodyAlignment: Alignment.center,

View File

@ -17,7 +17,7 @@ import 'package:spotube/hooks/utils/use_custom_status_bar_color.dart';
import 'package:spotube/hooks/utils/use_palette_color.dart';
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/provider/spotify/spotify.dart';
@ -84,7 +84,7 @@ class LyricsPage extends HookConsumerWidget {
final auth = ref.watch(authenticationProvider);
if (auth == null) {
if (auth.asData?.value == null) {
return Scaffold(
appBar: !kIsMacOS && !isModal ? const PageWindowTitleBar() : null,
body: const AnonymousFallback(),

View File

@ -14,7 +14,7 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/utils/use_force_update.dart';
import 'package:spotube/pages/lyrics/plain_lyrics.dart';
import 'package:spotube/pages/lyrics/synced_lyrics.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart';
@ -48,7 +48,7 @@ class MiniLyricsPage extends HookConsumerWidget {
final auth = ref.watch(authenticationProvider);
if (auth == null) {
if (auth.asData?.value == null) {
return const Scaffold(
appBar: PageWindowTitleBar(),
body: AnonymousFallback(),

View File

@ -3,7 +3,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/utils/platform.dart';
class WebViewLogin extends HookConsumerWidget {
@ -53,9 +53,7 @@ class WebViewLogin extends HookConsumerWidget {
final cookieHeader =
"sp_dc=${cookies.firstWhere((element) => element.name == "sp_dc").value}";
authenticationNotifier.setCredentials(
await AuthenticationCredentials.fromCookie(cookieHeader),
);
await authenticationNotifier.login(cookieHeader);
if (context.mounted) {
// ignore: use_build_context_synchronously
GoRouter.of(context).go("/");

View File

@ -20,7 +20,7 @@ import 'package:spotube/pages/search/sections/albums.dart';
import 'package:spotube/pages/search/sections/artists.dart';
import 'package:spotube/pages/search/sections/playlists.dart';
import 'package:spotube/pages/search/sections/tracks.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
@ -37,8 +37,7 @@ class SearchPage extends HookConsumerWidget {
final searchTerm = ref.watch(searchTermStateProvider);
final controller = useSearchController();
ref.watch(authenticationProvider);
final authenticationNotifier = ref.watch(authenticationProvider.notifier);
final auth = ref.watch(authenticationProvider);
final mediaQuery = MediaQuery.of(context);
final searchTrack = ref.watch(searchProvider(SearchType.track));
@ -91,7 +90,7 @@ class SearchPage extends HookConsumerWidget {
appBar: kIsDesktop && !kIsMacOS
? const PageWindowTitleBar(automaticallyImplyLeading: true)
: null,
body: !authenticationNotifier.isLoggedIn
body: auth.asData?.value == null
? const AnonymousFallback()
: Column(
children: [

View File

@ -9,7 +9,7 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/pages/profile/profile.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/scrobbler_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/service_utils.dart';
@ -35,7 +35,7 @@ class SettingsAccountSection extends HookConsumerWidget {
return SectionCardWithHeading(
heading: context.l10n.account,
children: [
if (auth != null)
if (auth.asData?.value != null)
ListTile(
leading: const Icon(SpotubeIcons.user),
title: const Text("User Profile"),
@ -53,7 +53,7 @@ class SettingsAccountSection extends HookConsumerWidget {
ServiceUtils.pushNamed(context, ProfilePage.name);
},
),
if (auth == null)
if (auth.asData?.value == null)
LayoutBuilder(builder: (context, constrains) {
return ListTile(
leading: Icon(

View File

@ -4,22 +4,30 @@ import 'dart:io';
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:drift/drift.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'
hide X509Certificate;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/routes.dart';
import 'package:spotube/components/dialogs/prompt_dialog.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/utils/persisted_state_notifier.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/utils/platform.dart';
class AuthenticationCredentials {
String cookie;
String accessToken;
DateTime expiration;
extension ExpirationAuthenticationTableData on AuthenticationTableData {
bool get isExpired => DateTime.now().isAfter(expiration);
String? getCookie(String key) => cookie.value
.split("; ")
.firstWhereOrNull((c) => c.trim().startsWith("$key="))
?.trim()
.split("=")
.last
.replaceAll(";", "");
}
class AuthenticationNotifier extends AsyncNotifier<AuthenticationTableData?> {
static final Dio dio = () {
final dio = Dio();
@ -32,13 +40,68 @@ class AuthenticationCredentials {
return dio;
}();
AuthenticationCredentials({
required this.cookie,
required this.accessToken,
required this.expiration,
@override
build() async {
final database = ref.watch(databaseProvider);
final data = await (database.select(database.authenticationTable)
..where((s) => s.id.equals(0)))
.getSingleOrNull();
Timer? refreshTimer;
ref.listenSelf((prevData, newData) async {
if (newData.asData?.value == null) return;
if (newData.asData!.value!.isExpired) {
await refreshCredentials();
}
// set the refresh timer
refreshTimer?.cancel();
refreshTimer = Timer(
newData.asData!.value!.expiration.difference(DateTime.now()),
() => refreshCredentials(),
);
});
static Future<AuthenticationCredentials> fromCookie(String cookie) async {
final subscription =
database.select(database.authenticationTable).watch().listen(
(event) {
state = AsyncData(event.isEmpty ? null : event.first);
},
);
ref.onDispose(() {
subscription.cancel();
refreshTimer?.cancel();
});
return data;
}
Future<void> refreshCredentials() async {
final database = ref.read(databaseProvider);
final refreshedCredentials =
await credentialsFromCookie(state.asData!.value!.cookie.value);
await database
.update(database.authenticationTable)
.replace(refreshedCredentials);
}
Future<void> login(String cookie) async {
final database = ref.read(databaseProvider);
final refreshedCredentials = await credentialsFromCookie(cookie);
await database
.into(database.authenticationTable)
.insert(refreshedCredentials);
}
Future<AuthenticationTableCompanion> credentialsFromCookie(
String cookie,
) async {
try {
final spDc = cookie
.split("; ")
@ -65,9 +128,10 @@ class AuthenticationCredentials {
);
}
return AuthenticationCredentials(
cookie: "${res.headers["set-cookie"]?.join(";")}; $spDc",
accessToken: body['accessToken'],
return AuthenticationTableCompanion.insert(
id: const Value(0),
cookie: DecryptedText("${res.headers["set-cookie"]?.join(";")}; $spDc"),
accessToken: DecryptedText(body['accessToken']),
expiration: DateTime.fromMillisecondsSinceEpoch(
body['accessTokenExpirationTimestampMs'],
),
@ -86,102 +150,20 @@ class AuthenticationCredentials {
}
}
/// Returns the cookie value
String? getCookie(String key) => cookie
.split("; ")
.firstWhereOrNull((c) => c.trim().startsWith("$key="))
?.trim()
.split("=")
.last
.replaceAll(";", "");
factory AuthenticationCredentials.fromJson(Map<String, dynamic> json) {
return AuthenticationCredentials(
cookie: json['cookie'] as String,
accessToken: json['accessToken'] as String,
expiration: DateTime.parse(json['expiration'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'cookie': cookie,
'accessToken': accessToken,
'expiration': expiration.toIso8601String(),
};
}
AuthenticationCredentials copyWith({
String? cookie,
String? accessToken,
DateTime? expiration,
}) {
return AuthenticationCredentials(
cookie: cookie ?? this.cookie,
accessToken: accessToken ?? this.accessToken,
expiration: expiration ?? this.expiration,
);
}
}
class AuthenticationNotifier
extends PersistedStateNotifier<AuthenticationCredentials?> {
bool get isLoggedIn => state != null;
AuthenticationNotifier() : super(null, "authentication", encrypted: true);
Timer? _refreshTimer;
@override
FutureOr<void> onInit() async {
super.onInit();
if (isLoggedIn && state!.isExpired) {
await refreshCredentials();
}
addListener((state) {
_refreshTimer?.cancel();
if (isLoggedIn && !state!.isExpired) {
_refreshTimer = Timer(
state.expiration.difference(DateTime.now()),
() => refreshCredentials(),
);
}
});
}
void setCredentials(AuthenticationCredentials credentials) {
state = credentials;
}
Future<void> logout() async {
state = null;
state = const AsyncData(null);
final database = ref.read(databaseProvider);
await (database.delete(database.authenticationTable)
..where((s) => s.id.equals(0)))
.go();
if (kIsMobile) {
WebStorageManager.instance().deleteAllData();
CookieManager.instance().deleteAllCookies();
}
}
Future<void> refreshCredentials() async {
if (!isLoggedIn) {
return;
}
state = await AuthenticationCredentials.fromCookie(state!.cookie);
}
@override
FutureOr<AuthenticationCredentials?> fromJson(Map<String, dynamic> json) {
return AuthenticationCredentials.fromJson(json);
}
@override
Map<String, dynamic> toJson() {
return state?.toJson() ?? {};
}
}
final authenticationProvider =
StateNotifierProvider<AuthenticationNotifier, AuthenticationCredentials?>(
(ref) => AuthenticationNotifier(),
AsyncNotifierProvider<AuthenticationNotifier, AuthenticationTableData?>(
() => AuthenticationNotifier(),
);

View File

@ -1,10 +1,10 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify_provider.dart';
import 'package:spotube/services/custom_spotify_endpoints/spotify_endpoints.dart';
final customSpotifyEndpointProvider = Provider<CustomSpotifyEndpoints>((ref) {
ref.watch(spotifyProvider);
final auth = ref.watch(authenticationProvider);
return CustomSpotifyEndpoints(auth?.accessToken ?? "");
return CustomSpotifyEndpoints(auth.asData?.value?.accessToken.value ?? "");
});

View File

@ -1,5 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
@ -8,7 +8,7 @@ final homeViewProvider = FutureProvider((ref) async {
userPreferencesProvider.select((s) => s.market),
);
final spTCookie = ref.watch(
authenticationProvider.select((s) => s?.getCookie("sp_t")),
authenticationProvider.select((s) => s.asData?.value?.getCookie("sp_t")),
);
if (spTCookie == null) return null;

View File

@ -1,6 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/models/spotify/home_feed.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/custom_spotify_endpoint_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
@ -11,7 +11,7 @@ final homeSectionViewProvider =
userPreferencesProvider.select((s) => s.market),
);
final spTCookie = ref.watch(
authenticationProvider.select((s) => s?.getCookie("sp_t")),
authenticationProvider.select((s) => s.asData?.value?.getCookie("sp_t")),
);
if (spTCookie == null) return null;

View File

@ -2,14 +2,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/env.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/utils/primitive_utils.dart';
final spotifyProvider = Provider<SpotifyApi>((ref) {
final authState = ref.watch(authenticationProvider);
final anonCred = PrimitiveUtils.getRandomElement(Env.spotifySecrets);
if (authState == null) {
if (authState.asData?.value == null) {
return SpotifyApi(
SpotifyApiCredentials(
anonCred["clientId"],
@ -18,5 +18,5 @@ final spotifyProvider = Provider<SpotifyApi>((ref) {
);
}
return SpotifyApi.withAccessToken(authState.accessToken);
return SpotifyApi.withAccessToken(authState.asData!.value!.accessToken.value);
});

View File

@ -134,8 +134,11 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
void setLocalLibraryLocation(List<String> localLibraryDirs) {
//if (localLibraryDir.isEmpty) return;
setData(PreferencesTableCompanion(
localLibraryLocation: Value(localLibraryDirs)));
setData(
PreferencesTableCompanion(
localLibraryLocation: Value(localLibraryDirs),
),
);
}
void setLayoutMode(LayoutMode mode) {

View File

@ -1,6 +1,7 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:uuid/uuid.dart';
import 'package:spotube/utils/platform.dart';
abstract class EncryptedKvStoreService {
static const _storage = FlutterSecureStorage(
@ -9,15 +10,21 @@ abstract class EncryptedKvStoreService {
),
);
static late final String _encryptionKeySync;
static String? _encryptionKeySync;
static Future<void> initialize() async {
_encryptionKeySync = await encryptionKey;
}
static String get encryptionKeySync => _encryptionKeySync;
static String get encryptionKeySync => _encryptionKeySync!;
static bool get isUnsupportedPlatform =>
kIsMacOS || kIsIOS || (kIsLinux && !kIsFlatpak);
static Future<String> get encryptionKey async {
if (isUnsupportedPlatform) {
return KVStoreService.encryptionKey;
}
try {
final value = await _storage.read(key: 'encryption');
final key = const Uuid().v4();
@ -34,10 +41,17 @@ abstract class EncryptedKvStoreService {
}
static Future<void> setEncryptionKey(String key) async {
if (isUnsupportedPlatform) {
await KVStoreService.setEncryptionKey(key);
return;
}
try {
await _storage.write(key: 'encryption', value: key);
} catch (e) {
await KVStoreService.setEncryptionKey(key);
} finally {
_encryptionKeySync = key;
}
}
}