Changes
3 changed files (+231/-4)
-
-
@@ -0,0 +1,120 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only package projection import ( "database/sql" "google.golang.org/protobuf/proto" "pocka.jp/x/yamori/backend/core/event" "pocka.jp/x/yamori/backend/core/snapshot" workspaceEventsv1 "pocka.jp/x/yamori/proto/go/backend/events/workspace/v1" workspace "pocka.jp/x/yamori/proto/go/backend/projections/workspace/v1" ) type Users struct { hasSnapshot bool eventSeq *uint64 Projection *workspace.Users } func NewUsers() *Users { return &Users{ hasSnapshot: false, eventSeq: nil, Projection: &workspace.Users{ Users: []*workspace.Users_User{}, }, } } func GetUsers(tx *sql.Tx) (*Users, error) { payload, seq, err := snapshot.GetLatest[workspace.Users](tx) if err != nil { return nil, err } if payload == nil { return NewUsers(), nil } return &Users{ hasSnapshot: true, eventSeq: &seq, Projection: payload, }, nil } func (p *Users) EventSeq() *uint64 { return p.eventSeq } func (p *Users) Update(events []event.Event) { if len(events) == 0 || p.Projection == nil { return } for _, container := range events { if p.eventSeq != nil && container.Seq <= *p.eventSeq { continue } ev := container.Payload.GetWorkspaceEvent() if ev == nil { continue } p.hasSnapshot = false switch ev := ev.Event.(type) { case *workspaceEventsv1.Event_UserCreated: p.Projection.Users = append(p.Projection.Users, &workspace.Users_User{ Id: ev.UserCreated.Id, Name: ev.UserCreated.Name, DisplayName: ev.UserCreated.DisplayName, KeyId: ev.UserCreated.KeyId, IsAdmin: proto.Bool(false), PasswordLogin: nil, }) case *workspaceEventsv1.Event_AdminAccessGranted: id := ev.AdminAccessGranted.GetUserId() for _, u := range p.Projection.Users { if u.GetId() == id { u.IsAdmin = proto.Bool(true) } } case *workspaceEventsv1.Event_AdminAccessRevoked: id := ev.AdminAccessRevoked.GetUserId() for _, u := range p.Projection.Users { if u.GetId() == id { u.IsAdmin = proto.Bool(false) } } case *workspaceEventsv1.Event_PasswordLoginConfigured: id := ev.PasswordLoginConfigured.GetUserId() for _, u := range p.Projection.Users { if u.GetId() == id { u.PasswordLogin = &workspace.Users_PasswordLogin{ Hash: ev.PasswordLoginConfigured.PasswordHash, Salt: ev.PasswordLoginConfigured.PasswordSalt, } } } } p.eventSeq = &container.Seq } } func (p *Users) SaveSnapshot(tx *sql.Tx) error { if p.hasSnapshot || p.eventSeq == nil { return nil } return snapshot.Save(tx, p.Projection, *p.eventSeq) }
-
-
-
@@ -37,11 +37,87 @@ func (s *Service) Login(ctx context.Context, req *connect.Request[workspaceV2.LoginRequest], ) (*connect.Response[workspaceV2.LoginResponse], error) { return connect.NewResponse(&workspaceV2.LoginResponse{ Result: &workspaceV2.LoginResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String("Not Implemented"), name := req.Msg.GetName() if name == "" { return connect.NewResponse(&workspaceV2.LoginResponse{ Result: &workspaceV2.LoginResponse_MissingFieldError{ MissingFieldError: &errorV1.MissingFieldError{ Path: proto.String("name"), }, }, }), nil } password := req.Msg.GetPassword() if password == "" { return connect.NewResponse(&workspaceV2.LoginResponse{ Result: &workspaceV2.LoginResponse_MissingFieldError{ MissingFieldError: &errorV1.MissingFieldError{ Path: proto.String("password"), }, }, }), nil } tx, err := s.core.DB.Begin() if err != nil { return nil, err } defer tx.Rollback() users, err := projection.GetUsers(tx) if err != nil { return nil, err } if err := event.UpdateProjections(tx, users); err != nil { return nil, err } for _, u := range users.Projection.Users { if u.GetName() != name { continue } if u.PasswordLogin == nil { return connect.NewResponse(&workspaceV2.LoginResponse{ Result: &workspaceV2.LoginResponse_AuthenticationError{ AuthenticationError: &errorV1.AuthenticationError{}, }, }), nil } hash := crypto.HashPassword([]byte(password), u.PasswordLogin.Salt) if !bytes.Equal(hash, u.PasswordLogin.Hash) { return connect.NewResponse(&workspaceV2.LoginResponse{ Result: &workspaceV2.LoginResponse_AuthenticationError{ AuthenticationError: &errorV1.AuthenticationError{}, }, }), nil } // TODO: JWT をSame-Origin Cookie に焼き付ける return connect.NewResponse(&workspaceV2.LoginResponse{ Result: &workspaceV2.LoginResponse_Ok{ Ok: &workspaceV2.User{ Id: &workspaceV2.UserID{ Value: u.Id, }, Name: u.Name, DisplayName: u.DisplayName, LoginMethod: &workspaceV2.LoginMethod{ PasswordConfigured: proto.Bool(true), }, IsAdmin: u.IsAdmin, }, }, }), nil } return connect.NewResponse(&workspaceV2.LoginResponse{ Result: &workspaceV2.LoginResponse_AuthenticationError{ AuthenticationError: &errorV1.AuthenticationError{}, }, }), nil }
-
-
-
@@ -0,0 +1,31 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only edition = "2023"; package yamori.backend.projections.workspace.v1; option go_package = "pocka.jp/x/yamori/proto/go/backend/projections/workspace/v1"; // ユーザの一覧。 // ぶっちゃけこれは普通のテーブルにしてもいいが、面倒なので Projection にしてる // パフォーマンスとかの問題が出てきたら Query 用のテーブルを作って Event -> SQL みたいな // Projection テーブルにするものいいかもしれない。クエリ・ストレージパフォーマンスと // マイグレーション容易性の妥協点的な。 message Users { message PasswordLogin { bytes hash = 1; bytes salt = 2; } message User { string id = 1; string name = 2; string display_name = 3; PasswordLogin password_login = 4; bool is_admin = 5; bytes key_id = 6; } repeated User users = 1; }
-