supabase_auth_service.dart 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. import 'dart:async';
  2. import 'package:appflowy/env/env.dart';
  3. import 'package:appflowy/startup/tasks/prelude.dart';
  4. import 'package:appflowy/user/application/auth/appflowy_auth_service.dart';
  5. import 'package:appflowy/user/application/auth/auth_service.dart';
  6. import 'package:appflowy/user/application/auth/device_id.dart';
  7. import 'package:appflowy/user/application/user_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:flutter/foundation.dart';
  14. import 'package:supabase_flutter/supabase_flutter.dart';
  15. import 'auth_error.dart';
  16. class SupabaseAuthService implements AuthService {
  17. SupabaseAuthService();
  18. SupabaseClient get _client => Supabase.instance.client;
  19. GoTrueClient get _auth => _client.auth;
  20. final AppFlowyAuthService _appFlowyAuthService = AppFlowyAuthService();
  21. @override
  22. Future<Either<FlowyError, UserProfilePB>> signUp({
  23. required String name,
  24. required String email,
  25. required String password,
  26. AuthTypePB authType = AuthTypePB.Supabase,
  27. Map<String, String> map = const {},
  28. }) async {
  29. if (!isSupabaseEnabled) {
  30. return _appFlowyAuthService.signUp(
  31. name: name,
  32. email: email,
  33. password: password,
  34. );
  35. }
  36. // fetch the uuid from supabase.
  37. final response = await _auth.signUp(
  38. email: email,
  39. password: password,
  40. );
  41. final uuid = response.user?.id;
  42. if (uuid == null) {
  43. return left(AuthError.supabaseSignUpError);
  44. }
  45. // assign the uuid to our backend service.
  46. // and will transfer this logic to backend later.
  47. return _appFlowyAuthService.signUp(
  48. name: name,
  49. email: email,
  50. password: password,
  51. authType: authType,
  52. map: {
  53. AuthServiceMapKeys.uuid: uuid,
  54. },
  55. );
  56. }
  57. @override
  58. Future<Either<FlowyError, UserProfilePB>> signIn({
  59. required String email,
  60. required String password,
  61. AuthTypePB authType = AuthTypePB.Supabase,
  62. Map<String, String> map = const {},
  63. }) async {
  64. if (!isSupabaseEnabled) {
  65. return _appFlowyAuthService.signIn(
  66. email: email,
  67. password: password,
  68. );
  69. }
  70. try {
  71. final response = await _auth.signInWithPassword(
  72. email: email,
  73. password: password,
  74. );
  75. final uuid = response.user?.id;
  76. if (uuid == null) {
  77. return Left(AuthError.supabaseSignInError);
  78. }
  79. return _appFlowyAuthService.signIn(
  80. email: email,
  81. password: password,
  82. authType: authType,
  83. map: {
  84. AuthServiceMapKeys.uuid: uuid,
  85. },
  86. );
  87. } on AuthException catch (e) {
  88. Log.error(e);
  89. return Left(AuthError.supabaseSignInError);
  90. }
  91. }
  92. @override
  93. Future<Either<FlowyError, UserProfilePB>> signUpWithOAuth({
  94. required String platform,
  95. AuthTypePB authType = AuthTypePB.Supabase,
  96. Map<String, String> map = const {},
  97. }) async {
  98. if (!isSupabaseEnabled) {
  99. return _appFlowyAuthService.signUpWithOAuth(platform: platform);
  100. }
  101. // Before signing in, sign out any existing users. Otherwise, the callback will be triggered even if the user doesn't click the 'Sign In' button on the website
  102. await _auth.signOut();
  103. final provider = platform.toProvider();
  104. final completer = supabaseLoginCompleter(
  105. onSuccess: (userId, userEmail) async {
  106. return await setupAuth(
  107. map: {
  108. AuthServiceMapKeys.uuid: userId,
  109. AuthServiceMapKeys.email: userEmail,
  110. AuthServiceMapKeys.deviceId: await getDeviceId()
  111. },
  112. );
  113. },
  114. );
  115. final response = await _auth.signInWithOAuth(
  116. provider,
  117. queryParams: queryParamsForProvider(provider),
  118. redirectTo: supabaseLoginCallback,
  119. );
  120. if (!response) {
  121. completer.complete(left(AuthError.supabaseSignInWithOauthError));
  122. }
  123. return completer.future;
  124. }
  125. @override
  126. Future<void> signOut({
  127. AuthTypePB authType = AuthTypePB.Supabase,
  128. }) async {
  129. if (isSupabaseEnabled) {
  130. await _auth.signOut();
  131. }
  132. await _appFlowyAuthService.signOut(
  133. authType: authType,
  134. );
  135. }
  136. @override
  137. Future<Either<FlowyError, UserProfilePB>> signUpAsGuest({
  138. AuthTypePB authType = AuthTypePB.Supabase,
  139. Map<String, String> map = const {},
  140. }) async {
  141. // supabase don't support guest login.
  142. // so, just forward to our backend.
  143. return _appFlowyAuthService.signUpAsGuest();
  144. }
  145. @override
  146. Future<Either<FlowyError, UserProfilePB>> signInWithMagicLink({
  147. required String email,
  148. Map<String, String> map = const {},
  149. }) async {
  150. final completer = supabaseLoginCompleter(
  151. onSuccess: (userId, userEmail) async {
  152. return await setupAuth(
  153. map: {
  154. AuthServiceMapKeys.uuid: userId,
  155. AuthServiceMapKeys.email: userEmail,
  156. AuthServiceMapKeys.deviceId: await getDeviceId()
  157. },
  158. );
  159. },
  160. );
  161. await _auth.signInWithOtp(
  162. email: email,
  163. emailRedirectTo: kIsWeb ? null : supabaseLoginCallback,
  164. );
  165. return completer.future;
  166. }
  167. @override
  168. Future<Either<FlowyError, UserProfilePB>> getUser() async {
  169. return UserBackendService.getCurrentUserProfile();
  170. }
  171. Future<Either<FlowyError, User>> getSupabaseUser() async {
  172. final user = _auth.currentUser;
  173. if (user == null) {
  174. return left(AuthError.supabaseGetUserError);
  175. }
  176. return Right(user);
  177. }
  178. Future<Either<FlowyError, UserProfilePB>> setupAuth({
  179. required Map<String, String> map,
  180. }) async {
  181. final payload = ThirdPartyAuthPB(
  182. authType: AuthTypePB.Supabase,
  183. map: map,
  184. );
  185. return UserEventThirdPartyAuth(payload)
  186. .send()
  187. .then((value) => value.swap());
  188. }
  189. }
  190. extension on String {
  191. Provider toProvider() {
  192. switch (this) {
  193. case 'github':
  194. return Provider.github;
  195. case 'google':
  196. return Provider.google;
  197. case 'discord':
  198. return Provider.discord;
  199. default:
  200. throw UnimplementedError();
  201. }
  202. }
  203. }
  204. /// Creates a completer that listens to Supabase authentication state changes and
  205. /// completes when a user signs in.
  206. ///
  207. /// This function sets up a listener on Supabase's authentication state. When a user
  208. /// signs in, it triggers the provided [onSuccess] callback with the user's `id` and
  209. /// `email`. Once the [onSuccess] callback is executed and a response is received,
  210. /// the completer completes with the response, and the listener is canceled.
  211. ///
  212. /// Parameters:
  213. /// - [onSuccess]: A callback function that's executed when a user signs in. It
  214. /// should take in a user's `id` and `email` and return a `Future` containing either
  215. /// a `FlowyError` or a `UserProfilePB`.
  216. ///
  217. /// Returns:
  218. /// A completer of type `Either<FlowyError, UserProfilePB>`. This completer completes
  219. /// with the response from the [onSuccess] callback when a user signs in.
  220. Completer<Either<FlowyError, UserProfilePB>> supabaseLoginCompleter({
  221. required Future<Either<FlowyError, UserProfilePB>> Function(
  222. String userId,
  223. String userEmail,
  224. ) onSuccess,
  225. }) {
  226. final completer = Completer<Either<FlowyError, UserProfilePB>>();
  227. late final StreamSubscription<AuthState> subscription;
  228. final auth = Supabase.instance.client.auth;
  229. subscription = auth.onAuthStateChange.listen((event) async {
  230. final user = event.session?.user;
  231. if (event.event == AuthChangeEvent.signedIn && user != null) {
  232. final response = await onSuccess(
  233. user.id,
  234. user.email ?? user.newEmail ?? '',
  235. );
  236. // Only cancel the subscription if the Event is signedIn.
  237. subscription.cancel();
  238. completer.complete(response);
  239. }
  240. });
  241. return completer;
  242. }
  243. Map<String, String> queryParamsForProvider(Provider provider) {
  244. switch (provider) {
  245. case Provider.github:
  246. return {};
  247. case Provider.google:
  248. return {
  249. 'access_type': 'offline',
  250. 'prompt': 'consent',
  251. };
  252. case Provider.discord:
  253. return {};
  254. default:
  255. return {};
  256. }
  257. }