😇

StorageException(message: new row violates row-level security policy,

2024/02/12に公開

💡Tips

Supabaseで画像のupload処理を実行するとエラーが出た???
コードが正しくないのもあったけど、ポリシーの許可なるものが必要だった。

過去に書いた記事を参考に解決していく。
https://zenn.dev/joo_hashi/articles/fe02fa0036af7b

こんなエラーに遭遇した!、他にもあるけれど問題なのは、認証がされているユーザーでないとアップロードがそもそも許可されないエラーに遭遇した。

lutter: │ 🐛 😇upload error: StorageException(message: new row violates row-level security policy, statusCode: 403, error: Unauthorized) flutter: └─────────────────────────────────────────────────────────────────────

📡403についてですが、これは認可がされていないので、アクセスを許可しませんって意味ですね。HTTP通信をREST APIとしているとよく起きるエラーですね。ログインしてると認証はされてる。でも、DBへの書き込みや読み取りは、認可がされないと許可されない。

今回認可を許可してもらうのに必要なもの

  1. user_id
  2. fileName

コードだとこんな感じかな。ユーザーの情報が取れれば403のエラーは解決できる。

final session = SupabaseInstance().supabase.client.auth.currentSession;
final user = session?.user;
final fileName = '${DateTime.now().toIso8601String()}.$fileExt';

https://developer.mozilla.org/ja/docs/Web/HTTP/Status/403

ソースコードはこれですね。Supabaseってシングルトンだからプロバイダーで呼び出せないんですよね。この書き方も良いとはいえないかもだけど、毎回Supabase.instance.clientと書きたくない。
コードの記述量も減ってない気がするからこれやめた方がいいかもしれない...
これは実験用です。

import 'package:supabase_flutter/supabase_flutter.dart';

// 他のクラスで、Supabaseのインスタンスを使いたい場合に使うMixin
mixin SupabaseMixin {
  final Supabase supabase = Supabase.instance;
}

// riverpodのプロバイダーで定義できないのでクラスを使う
class SupabaseInstance with SupabaseMixin {}

これは、まだ未完成ですが、画像のIconButtonを押すと実行されるロジックを作ってみました!

import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

import '../core/logger/logger.dart';
import '../core/supabase/supabase_mixin.dart';

part 'user_repository.g.dart';

(keepAlive: true)
UserRepository userRepository(UserRepositoryRef ref) {
  return UserRepository(ref);
}

class UserRepository {
  UserRepository(this.ref);
  final Ref ref;

  FutureOr<void> uploadImage() async {
    final picker = ImagePicker();
    final imageFile = await picker.pickImage(
      source: ImageSource.gallery,
      maxWidth: 300,
      maxHeight: 300,
    );
    if (imageFile == null) {
      return;
    }
    // storageに画像をアップロード
    try {
      final bytes = await imageFile.readAsBytes();
      final fileExt = imageFile.path.split('.').last;
      final session = SupabaseInstance().supabase.client.auth.currentSession;
      final user = session?.user;
      final fileName = '${DateTime.now().toIso8601String()}.$fileExt';
      final filePath = '${user?.id}/$fileName';
      await SupabaseInstance()
          .supabase
          .client
          .storage
          .from('avatars')
          .uploadBinary(
            filePath,
            bytes,
            fileOptions: FileOptions(contentType: imageFile.mimeType),
          );
      // アップロードした画像のURLを取得
      logger.d('📁filePath: $filePath');
      // 権限を必要とせずにファイルをダウンロードするための署名付き URL を作成
      final imageUrlResponse = await SupabaseInstance()
          .supabase
          .client
          .storage
          .from('avatars')
          .createSignedUrl(filePath, 60 * 60 * 24 * 365 * 10);
      return;
    } on StorageException catch (e) {
      logger.d('😇upload error: $e');
      return;
    }
  }
}

uploadするボタンはこれだけあれば作れます。

IconButton(
    onPressed: () async {
      await ref.read(userRepositoryProvider).uploadImage();
    },
    icon: const Icon(Icons.person),
  ),

成功するとこのように画像がuploadされます!

まとめ

短い記事ですが、Supabaseってあまり情報がないのでメモ用に情報を残そうと記事を書きました。ポリシーやローレベルセキュリティの許可が必要なところでよくハマりますね。

Discussion