supabase_auth_service.dart 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import 'dart:async';
  2. import 'package:appflowy/core/config/kv.dart';
  3. import 'package:appflowy/core/config/kv_keys.dart';
  4. import 'package:appflowy/startup/startup.dart';
  5. import 'package:appflowy/startup/tasks/prelude.dart';
  6. import 'package:appflowy/user/application/auth/appflowy_auth_service.dart';
  7. import 'package:appflowy/user/application/auth/auth_service.dart';
  8. import 'package:appflowy_backend/dispatch/dispatch.dart';
  9. import 'package:appflowy_backend/log.dart';
  10. import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
  11. import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
  12. import 'package:dartz/dartz.dart';
  13. import 'package:supabase_flutter/supabase_flutter.dart';
  14. import 'auth_error.dart';
  15. class SupabaseAuthService implements AuthService {
  16. SupabaseAuthService();
  17. SupabaseClient get _client => Supabase.instance.client;
  18. GoTrueClient get _auth => _client.auth;
  19. final AppFlowyAuthService _appFlowyAuthService = AppFlowyAuthService();
  20. @override
  21. Future<Either<FlowyError, UserProfilePB>> signUp({
  22. required String name,
  23. required String email,
  24. required String password,
  25. AuthTypePB authType = AuthTypePB.Supabase,
  26. Map<String, String> map = const {},
  27. }) async {
  28. if (!isSupabaseEnable) {
  29. return _appFlowyAuthService.signUp(
  30. name: name,
  31. email: email,
  32. password: password,
  33. );
  34. }
  35. // fetch the uuid from supabase.
  36. final response = await _auth.signUp(
  37. email: email,
  38. password: password,
  39. );
  40. final uuid = response.user?.id;
  41. if (uuid == null) {
  42. return left(AuthError.supabaseSignUpError);
  43. }
  44. // assign the uuid to our backend service.
  45. // and will transfer this logic to backend later.
  46. return _appFlowyAuthService.signUp(
  47. name: name,
  48. email: email,
  49. password: password,
  50. authType: authType,
  51. map: {
  52. AuthServiceMapKeys.uuid: uuid,
  53. },
  54. );
  55. }
  56. @override
  57. Future<Either<FlowyError, UserProfilePB>> signIn({
  58. required String email,
  59. required String password,
  60. AuthTypePB authType = AuthTypePB.Supabase,
  61. Map<String, String> map = const {},
  62. }) async {
  63. if (!isSupabaseEnable) {
  64. return _appFlowyAuthService.signIn(
  65. email: email,
  66. password: password,
  67. );
  68. }
  69. try {
  70. final response = await _auth.signInWithPassword(
  71. email: email,
  72. password: password,
  73. );
  74. final uuid = response.user?.id;
  75. if (uuid == null) {
  76. return Left(AuthError.supabaseSignInError);
  77. }
  78. return _appFlowyAuthService.signIn(
  79. email: email,
  80. password: password,
  81. authType: authType,
  82. map: {
  83. AuthServiceMapKeys.uuid: uuid,
  84. },
  85. );
  86. } on AuthException catch (e) {
  87. Log.error(e);
  88. return Left(AuthError.supabaseSignInError);
  89. }
  90. }
  91. @override
  92. Future<Either<FlowyError, UserProfilePB>> signUpWithOAuth({
  93. required String platform,
  94. AuthTypePB authType = AuthTypePB.Supabase,
  95. Map<String, String> map = const {},
  96. }) async {
  97. if (!isSupabaseEnable) {
  98. return _appFlowyAuthService.signUpWithOAuth(
  99. platform: platform,
  100. );
  101. }
  102. final provider = platform.toProvider();
  103. final completer = Completer<Either<FlowyError, UserProfilePB>>();
  104. late final StreamSubscription<AuthState> subscription;
  105. subscription = _auth.onAuthStateChange.listen((event) async {
  106. if (event.event != AuthChangeEvent.signedIn) {
  107. completer.complete(left(AuthError.supabaseSignInWithOauthError));
  108. } else {
  109. final user = await getSupabaseUser();
  110. final Either<FlowyError, UserProfilePB> response = await user.fold(
  111. (l) => left(l),
  112. (r) async => await setupAuth(map: {AuthServiceMapKeys.uuid: r.id}),
  113. );
  114. completer.complete(response);
  115. }
  116. subscription.cancel();
  117. });
  118. final Map<String, String> query = {};
  119. if (provider == Provider.google) {
  120. query['access_type'] = 'offline';
  121. query['prompt'] = 'consent';
  122. }
  123. final response = await _auth.signInWithOAuth(
  124. provider,
  125. queryParams: query,
  126. redirectTo:
  127. 'io.appflowy.appflowy-flutter://login-callback', // can't use underscore here.
  128. );
  129. if (!response) {
  130. completer.complete(left(AuthError.supabaseSignInWithOauthError));
  131. }
  132. return completer.future;
  133. }
  134. @override
  135. Future<void> signOut({
  136. AuthTypePB authType = AuthTypePB.Supabase,
  137. }) async {
  138. if (!isSupabaseEnable) {
  139. return _appFlowyAuthService.signOut();
  140. }
  141. await _auth.signOut();
  142. await _appFlowyAuthService.signOut(
  143. authType: authType,
  144. );
  145. }
  146. @override
  147. Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
  148. AuthTypePB authType = AuthTypePB.Supabase,
  149. Map<String, String> map = const {},
  150. }) async {
  151. // supabase don't support guest login.
  152. // so, just forward to our backend.
  153. return _appFlowyAuthService.signUpAsGuest();
  154. }
  155. @override
  156. Future<Either<FlowyError, UserProfilePB>> getUser() async {
  157. final loginType = await getIt<KeyValueStorage>()
  158. .get(KVKeys.loginType)
  159. .then((value) => value.toOption().toNullable());
  160. if (!isSupabaseEnable || (loginType != null && loginType != 'supabase')) {
  161. return _appFlowyAuthService.getUser();
  162. }
  163. final user = await getSupabaseUser();
  164. return user.map((r) => r.toUserProfile());
  165. }
  166. Future<Either<FlowyError, User>> getSupabaseUser() async {
  167. final user = _auth.currentUser;
  168. if (user == null) {
  169. return left(AuthError.supabaseGetUserError);
  170. }
  171. return Right(user);
  172. }
  173. Future<Either<FlowyError, UserProfilePB>> setupAuth({
  174. required Map<String, String> map,
  175. }) async {
  176. final payload = ThirdPartyAuthPB(
  177. authType: AuthTypePB.Supabase,
  178. map: map,
  179. );
  180. return UserEventThirdPartyAuth(payload)
  181. .send()
  182. .then((value) => value.swap());
  183. }
  184. }
  185. extension on User {
  186. UserProfilePB toUserProfile() {
  187. return UserProfilePB()
  188. ..email = email ?? ''
  189. ..token = this.id;
  190. }
  191. }
  192. extension on String {
  193. Provider toProvider() {
  194. switch (this) {
  195. case 'github':
  196. return Provider.github;
  197. case 'google':
  198. return Provider.google;
  199. case 'discord':
  200. return Provider.discord;
  201. default:
  202. throw UnimplementedError();
  203. }
  204. }
  205. }