Unverified Commit d688914b authored by boojack's avatar boojack Committed by GitHub

feat(auth): add SSO user identity linkage (#5883)

parent 50638040
...@@ -9,9 +9,6 @@ build/ ...@@ -9,9 +9,6 @@ build/
bin/ bin/
memos memos
# Plan/design documents
docs/plans/
.DS_Store .DS_Store
# Jetbrains # Jetbrains
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
SSO sign-in in memos currently treats the IdP-provided identifier as the local username. The identifier value comes from the OAuth2 UserInfo claim named in `FieldMapping.identifier`, while local usernames are validated by `validateUsername` against `base.UIDMatcher`. Real IdPs frequently emit identifiers such as email addresses, opaque subject IDs, or provider-specific account IDs that are valid authentication subjects but are not valid memos usernames. SSO sign-in in memos currently treats the IdP-provided identifier as the local username. The identifier value comes from the OAuth2 UserInfo claim named in `FieldMapping.identifier`, while local usernames are validated by `validateUsername` against `base.UIDMatcher`. Real IdPs frequently emit identifiers such as email addresses, opaque subject IDs, or provider-specific account IDs that are valid authentication subjects but are not valid memos usernames.
The existing issue artifacts under `docs/issues/2026-04-21-sso-user-identity-linkage/` already scope a persistent linkage between SSO identities and local users. A broader review of upstream open source schemas now shows that similar systems converge on separating external identity from the local user row, but do not converge on one universal table name or one exact column set. That difference matters because the implementation problem is narrower than "copy one upstream schema exactly" and broader than "pick any new table name locally." The existing issue artifacts under `docs/plans/2026-04-21-sso-user-identity-linkage/` already scope a persistent linkage between SSO identities and local users. A broader review of upstream open source schemas now shows that similar systems converge on separating external identity from the local user row, but do not converge on one universal table name or one exact column set. That difference matters because the implementation problem is narrower than "copy one upstream schema exactly" and broader than "pick any new table name locally."
## Issue Statement ## Issue Statement
......
## Execution Log
### T1: Add `user_identity` migrations + LATEST.sql updates
**Status**: Completed
**Files Changed**:
- Created: `store/migration/sqlite/0.28/00__user_identity.sql`
- Created: `store/migration/postgres/0.28/00__user_identity.sql`
- Created: `store/migration/mysql/0.28/00__user_identity.sql`
- Modified: `store/migration/sqlite/LATEST.sql`
- Modified: `store/migration/postgres/LATEST.sql`
- Modified: `store/migration/mysql/LATEST.sql`
**Validation**:
- `rg 'CREATE TABLE \`?user_identity\`?' store/migration` — PASS (hits in all 6 expected files).
- `rg 'UNIQUE \(\`?provider\`?, \`?extern_uid\`?\)' store/migration` — PASS (6 hits).
- `go build ./...` — PASS.
**Path Corrections**: None.
**Deviations**: None.
### T2: Add `store.UserIdentity` model, `Store` methods, and driver interface
**Status**: Completed
**Files Changed**:
- Created: `store/user_identity.go`
- Modified: `store/driver.go`
**Validation**:
- Interface-only build is expected to fail until T3–T5; deferred compile check to T5.
- `rg 'CreateUserIdentity|ListUserIdentities' store/driver.go store/user_identity.go` — PASS (method declarations present in both files).
**Path Corrections**: None.
**Deviations**: None.
### T3: Implement SQLite driver for `user_identity`
**Status**: Completed
**Files Changed**:
- Created: `store/db/sqlite/user_identity.go`
**Validation**:
- `go build ./store/db/sqlite/...` — PASS.
**Path Corrections**: None.
**Deviations**: None.
### T4: Implement Postgres driver for `user_identity`
**Status**: Completed
**Files Changed**:
- Created: `store/db/postgres/user_identity.go`
**Validation**:
- `go build ./store/db/postgres/...` — PASS.
**Path Corrections**: None.
**Deviations**: None.
### T5: Implement MySQL driver for `user_identity`
**Status**: Completed
**Files Changed**:
- Created: `store/db/mysql/user_identity.go`
**Validation**:
- `go build ./...` — PASS (whole repo compiles; all drivers satisfy the `Driver` interface).
**Path Corrections**: None.
**Deviations**: None.
### T6: Add store-layer tests for `user_identity`
**Status**: Completed
**Files Changed**:
- Created: `store/test/user_identity_test.go`
**Validation**:
- `DRIVER=sqlite go test ./store/test/ -run TestUserIdentity -count=1 -v` — PASS:
- `TestUserIdentityCreateAndGet` — PASS
- `TestUserIdentityListByUserID` — PASS
- `TestUserIdentityUniqueConflict` — PASS
- `TestUserIdentitySameExternUIDDifferentProviders` — PASS
**Path Corrections**: None.
**Deviations**: None.
### T7: Add SSO username derivation helper
**Status**: Completed
**Files Changed**:
- Created: `server/router/api/v1/sso_username.go`
**Validation**:
- `go build ./server/router/api/v1/...` — PASS.
- `go vet ./server/router/api/v1/...` — PASS.
**Path Corrections**: None.
**Deviations**: None.
### T8: Route SSO sign-in through `user_identity` linkage
**Status**: Completed
**Files Changed**:
- Modified: `server/router/api/v1/auth_service.go`
- `SignIn` SSO branch now delegates user resolution to a new `resolveSSOUser` method.
- `resolveSSOUser` does: `user_identity` lookup → hit path (load user by linked `user_id`); miss path (registration gate → `deriveSSOUsername` → create user → create linkage → race recovery on unique(provider, extern_uid)).
- Added `isUserIdentityUniqueViolation` helper (string match on the three backends' unique-constraint error strings, matching the pattern in `memo_service.go:103–105`).
**Validation**:
- `go build ./...` — PASS.
- `go vet ./...` — PASS.
- `DRIVER=sqlite go test ./store/test/ -run TestUserIdentity -count=1` — PASS (regression check).
**Path Corrections**:
- The plan pseudocode referenced `identityProvider.UID`; the actual protobuf type `storepb.IdentityProvider` exposes the field as `Uid`. Used `identityProvider.Uid` in the implementation. No semantic deviation.
**Deviations**: None.
## Completion Declaration
**All tasks completed successfully.**
## Task List
**Task Index**
> T1: Add `user_identity` migrations + LATEST.sql updates for all three backends [M] — T2: Add `store.UserIdentity` model and `Store` methods + driver interface [M] — T3: Implement SQLite driver for `user_identity` [M] — T4: Implement Postgres driver for `user_identity` [M] — T5: Implement MySQL driver for `user_identity` [M] — T6: Add store-layer tests for `user_identity` [M] — T7: Add SSO username derivation helper [M] — T8: Route SSO sign-in through `user_identity` linkage [L]
### T1: Add `user_identity` migrations + LATEST.sql updates [M]
**Objective**: Create the `user_identity` persistence structure across SQLite, Postgres, and MySQL, and reflect it in `LATEST.sql` for fresh installs (G1, G2, G3, G4, G5; design §1, §5).
**Size**: M (3 new migration files, 3 LATEST.sql edits; straightforward DDL).
**Files**:
- Create: `store/migration/sqlite/0.28/00__user_identity.sql`
- Create: `store/migration/postgres/0.28/00__user_identity.sql`
- Create: `store/migration/mysql/0.28/00__user_identity.sql`
- Modify: `store/migration/sqlite/LATEST.sql`
- Modify: `store/migration/postgres/LATEST.sql`
- Modify: `store/migration/mysql/LATEST.sql`
**Implementation**:
1. `store/migration/sqlite/0.28/00__user_identity.sql`:
```sql
CREATE TABLE user_identity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
provider TEXT NOT NULL,
extern_uid TEXT NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE (provider, extern_uid)
);
CREATE INDEX idx_user_identity_user_id ON user_identity(user_id);
```
2. `store/migration/postgres/0.28/00__user_identity.sql`: same logical schema with Postgres types — `id SERIAL PRIMARY KEY`, `user_id INTEGER NOT NULL`, `provider TEXT NOT NULL`, `extern_uid TEXT NOT NULL`, `created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())`, `updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())`, `UNIQUE(provider, extern_uid)`, plus `CREATE INDEX idx_user_identity_user_id ON user_identity(user_id);`. Include a 2-line header comment describing the table purpose (pattern-match `04__memo_share.sql`).
3. `store/migration/mysql/0.28/00__user_identity.sql`: same logical schema with MySQL syntax — backticked identifiers, `INT NOT NULL AUTO_INCREMENT PRIMARY KEY`, `VARCHAR(256)` for `provider`, `VARCHAR(256)` for `extern_uid` (so unique key fits within index limits), `BIGINT NOT NULL DEFAULT (UNIX_TIMESTAMP())` for timestamps, `UNIQUE(provider, extern_uid)`, plus `CREATE INDEX idx_user_identity_user_id ON user_identity(user_id);`.
4. Append a `-- user_identity` section to each `LATEST.sql` mirroring the corresponding migration file (schema only, same indentation style used by neighboring tables in that file).
**Boundaries**: Must NOT alter the `user` or `idp` tables; must NOT add FK from `user_identity.provider` to `idp.uid`; must NOT add columns beyond `id`, `user_id`, `provider`, `extern_uid`, `created_ts`, `updated_ts`.
**Dependencies**: None.
**Expected Outcome**: New migration files exist; `LATEST.sql` for each backend contains a `user_identity` table block and its `user_id` index.
**Validation**:
- `rg -n "CREATE TABLE user_identity" store/migration` — expects one hit per backend in both the 0.28 migration and `LATEST.sql` (6 hits total).
- `rg -n "UNIQUE ?\\(provider, extern_uid\\)" store/migration` — expects 6 hits total.
- `go build ./...` — expects PASS (no code changes affect the build; confirms no stray syntax issues).
---
### T2: Add `store.UserIdentity` model, `Store` methods, and driver interface [M]
**Objective**: Provide a Go-level abstraction for the `user_identity` record with create/read operations wired through `store.Driver` (design §2, G3, G5).
**Size**: M (one new store file, one interface edit; simple CRUD-shaped code).
**Files**:
- Create: `store/user_identity.go`
- Modify: `store/driver.go`
**Implementation**:
1. `store/user_identity.go`:
- Types:
```go
type UserIdentity struct {
ID int32
UserID int32
Provider string
ExternUID string
CreatedTs int64
UpdatedTs int64
}
type FindUserIdentity struct {
ID *int32
UserID *int32
Provider *string
ExternUID *string
}
```
- Store methods (thin passthroughs to driver):
```go
func (s *Store) CreateUserIdentity(ctx context.Context, create *UserIdentity) (*UserIdentity, error)
func (s *Store) ListUserIdentities(ctx context.Context, find *FindUserIdentity) ([]*UserIdentity, error)
func (s *Store) GetUserIdentity(ctx context.Context, find *FindUserIdentity) (*UserIdentity, error) // returns (nil, nil) on no match
```
- No update/delete methods in this issue (design §2: create/read only).
2. `store/driver.go`: extend the `Driver` interface with:
```go
// UserIdentity model related methods.
CreateUserIdentity(ctx context.Context, create *UserIdentity) (*UserIdentity, error)
ListUserIdentities(ctx context.Context, find *FindUserIdentity) ([]*UserIdentity, error)
```
`GetUserIdentity` in `store` can be implemented locally by calling `ListUserIdentities` with `Limit`-free semantics and returning the first row, matching the `GetMemoShare`/`GetIdentityProvider` pattern (no new driver method required for "get").
**Boundaries**: Must NOT add fields to `store.User` or `store.UpdateUser`; must NOT add update/delete methods.
**Dependencies**: None (T3–T5 will satisfy the new interface methods).
**Expected Outcome**: `store.UserIdentity`, `FindUserIdentity`, and three `Store` methods exist; `Driver` interface declares the two new methods.
**Validation**:
- `go build ./store/...` — expects FAIL until T3–T5 implement the interface on each driver. Record as expected; final pass comes at end of T5.
- `rg -n "CreateUserIdentity|ListUserIdentities" store/driver.go store/user_identity.go` — expects method declarations in both files.
---
### T3: Implement SQLite driver for `user_identity` [M]
**Objective**: Implement `CreateUserIdentity` and `ListUserIdentities` for SQLite so the interface declared in T2 is satisfied (design §2).
**Size**: M (one new driver file; mirrors existing `memo_share.go` patterns).
**Files**:
- Create: `store/db/sqlite/user_identity.go`
**Implementation**:
1. `CreateUserIdentity`:
- Insert columns `user_id`, `provider`, `extern_uid` using `?` placeholders.
- Use `RETURNING id, created_ts, updated_ts` to populate generated fields, same pattern as `store/db/sqlite/memo_share.go:24`.
- Return the passed-in `create` struct with generated fields populated, or the error from `QueryRowContext(...).Scan(...)` (unique-constraint violation surfaces to caller unchanged).
2. `ListUserIdentities`:
- `where := []string{"1 = 1"}`; append clauses for `find.ID`, `find.UserID`, `find.Provider`, `find.ExternUID` when non-nil.
- `SELECT id, user_id, provider, extern_uid, created_ts, updated_ts FROM user_identity WHERE ... ORDER BY id ASC`.
- Scan rows into `[]*store.UserIdentity`; return `[]*store.UserIdentity{}` on no rows (not nil).
**Boundaries**: Must NOT introduce transaction helpers, upsert semantics, or extra scan columns.
**Dependencies**: T2.
**Expected Outcome**: SQLite driver compiles and returns populated rows.
**Validation**:
- `go build ./store/db/sqlite/...` — expects PASS.
---
### T4: Implement Postgres driver for `user_identity` [M]
**Objective**: Mirror T3 for Postgres using `$N` placeholders and `SERIAL` semantics (design §2).
**Size**: M (one new driver file; mirrors `store/db/postgres/memo_share.go`).
**Files**:
- Create: `store/db/postgres/user_identity.go`
**Implementation**:
- Same shape as T3, but:
- Use `placeholder(n)` / `placeholders(n)` helpers from `store/db/postgres/common.go`.
- Insert stmt `INSERT INTO user_identity (user_id, provider, extern_uid) VALUES (...) RETURNING id, created_ts, updated_ts`.
- List query identical SQL shape to SQLite (no backticks in Postgres; match `memo_share.go` style).
**Boundaries**: Same as T3.
**Dependencies**: T2.
**Expected Outcome**: Postgres driver compiles.
**Validation**:
- `go build ./store/db/postgres/...` — expects PASS.
---
### T5: Implement MySQL driver for `user_identity` [M]
**Objective**: Mirror T3/T4 for MySQL, using `LastInsertId()` + re-read pattern (MySQL's driver does not support `RETURNING`; design §2).
**Size**: M (one new driver file; mirrors `store/db/mysql/memo_share.go`).
**Files**:
- Create: `store/db/mysql/user_identity.go`
**Implementation**:
- `CreateUserIdentity`:
- `INSERT INTO user_identity (user_id, provider, extern_uid) VALUES (?, ?, ?)` via `ExecContext`.
- Get `LastInsertId()`, re-fetch via `GetUserIdentity(... ID: &id)` helper (internal unexported `listUserIdentitiesByID` or reuse `ListUserIdentities` with `FindUserIdentity{ID: &id}` + take first result).
- Mirror `memo_share.go` error-handling style (return `errors.Errorf("failed to create user identity")` when re-fetch returns nil, like memo_share does).
- `ListUserIdentities`:
- Same shape as T3, using backticked column names (`` `user_id` ``, `` `provider` ``, `` `extern_uid` ``) and `?` placeholders, matching the MySQL idiom used in `memo_share.go`.
**Boundaries**: Same as T3.
**Dependencies**: T2.
**Expected Outcome**: MySQL driver compiles; full repo builds.
**Validation**:
- `go build ./...` — expects PASS (entire repo compiles with all drivers satisfying the `Driver` interface introduced in T2).
---
### T6: Add store-layer tests for `user_identity` [M]
**Objective**: Exercise create + read paths plus the `(provider, extern_uid)` uniqueness guard across the active driver (G2).
**Size**: M (one new test file; patterns match existing store tests).
**Files**:
- Create: `store/test/user_identity_test.go`
**Implementation**:
1. `TestUserIdentityCreateAndGet`:
- Create host user via `createTestingHostUser`.
- `CreateUserIdentity` with `UserID=user.ID`, `Provider="idp-uid-1"`, `ExternUID="jane@example.com"`.
- `GetUserIdentity` by `(Provider, ExternUID)` — assert match on `UserID`, `Provider`, `ExternUID`, non-zero `ID`, non-zero `CreatedTs`.
2. `TestUserIdentityListByUserID`:
- Create two identities under the same `UserID` with two different `Provider` values.
- `ListUserIdentities` by `UserID` — assert length 2.
3. `TestUserIdentityUniqueConflict`:
- Insert one row with `(Provider="idp-A", ExternUID="sub-1")`.
- Insert a second row with identical `(Provider, ExternUID)` for a different `UserID`.
- Assert the second `CreateUserIdentity` returns a non-nil error (detection via `err != nil`; do not assert message since error strings differ per backend).
4. `TestUserIdentitySameExternUIDDifferentProviders`:
- Insert `(Provider="idp-A", ExternUID="sub-1")` and `(Provider="idp-B", ExternUID="sub-1")` under the same or different users.
- Assert both inserts succeed (G2: uniqueness is scoped to the pair, not `extern_uid` alone).
**Boundaries**: Must NOT test SSO sign-in or auth service behavior; must NOT test migration contents beyond what `NewTestingStore` already executes.
**Dependencies**: T1–T5.
**Expected Outcome**: All four tests pass against SQLite.
**Validation**:
- `go test ./store/test/ -run TestUserIdentity -count=1` — expects all 4 tests PASS.
---
### T7: Add SSO username derivation helper [M]
**Objective**: Produce a valid `User.Username` for new SSO-created users from profile fields, independent of `extern_uid` (design §4).
**Size**: M (one new file with helper + small unit test; self-contained logic).
**Files**:
- Create: `server/router/api/v1/sso_username.go`
**Implementation**:
1. `deriveSSOUsername(ctx context.Context, stores *store.Store, userInfo *idp.IdentityProviderUserInfo) (string, error)`:
- Build ordered candidate list: `[userInfo.DisplayName, userInfo.Email, userInfo.Identifier]`, skipping empty values.
- For each candidate:
1. `base := normalizeToUsername(candidate)`
2. If `validateUsername(base) == nil`:
- If no existing user with `Username=base` (via `stores.GetUser(&FindUser{Username: &base})`), return `base`.
- Else: try up to N=8 suffix retries `base + "-" + randomSuffix(6)`, where the trimmed base ensures total length ≤ 36. If a candidate passes `validateUsername` and is unique, return it.
3. If all candidates are exhausted: fall back to a purely random username `"user-" + randomSuffix(10)` validated via `validateUsername`; retry up to 5 times before returning an error.
4. `normalizeToUsername(s string) string`:
- ASCII-fold / lowercase.
- Replace every character not in `[a-zA-Z0-9]` with `-`.
- Collapse consecutive `-` into one `-`.
- Trim leading/trailing `-`.
- Truncate to 36 chars, then re-trim trailing `-` so the string still ends in alphanumeric.
- Return `""` if the result is empty or fully numeric (so the caller falls through to the next candidate).
5. Use `internal/util.RandomString` for the random suffix (already imported by `auth_service.go`).
**Boundaries**: Must NOT modify `validateUsername` or `base.UIDMatcher`; must NOT write to `user_identity` or `user` directly; must NOT call `CreateUser`.
**Dependencies**: None.
**Expected Outcome**: New file `server/router/api/v1/sso_username.go` containing the exported-for-package helper `deriveSSOUsername` and internal `normalizeToUsername`.
**Validation**:
- `go build ./server/router/api/v1/...` — expects PASS.
- `go vet ./server/router/api/v1/...` — expects PASS.
---
### T8: Route SSO sign-in through `user_identity` linkage [L]
**Objective**: Replace the `FindUser{Username: &userInfo.Identifier}` lookup and `Username: userInfo.Identifier` user creation with `user_identity`-backed lookup and derived-username user creation, satisfying G1 and G2 end-to-end (design §3).
**Size**: L (non-trivial branching logic: lookup, miss path, registration gate, race recovery).
**Files**:
- Modify: `server/router/api/v1/auth_service.go`
**Implementation** (in `SignIn`, SSO branch, replacing current lines ~124–173):
1. After `identifier_filter` check succeeds (existing `lines 124-133` unchanged), resolve the linkage:
```go
provider := identityProvider.Uid
externUID := userInfo.Identifier
existingIdentity, err := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
Provider: &provider,
ExternUID: &externUID,
})
// error handling → codes.Internal
```
2. **Hit path**: if `existingIdentity != nil`, load `s.Store.GetUser(ctx, &store.FindUser{ID: &existingIdentity.UserID})`; set `existingUser`; skip creation.
3. **Miss path**: gate on `instanceGeneralSetting.DisallowUserRegistration` (reuse existing flow at current lines 143–149), then:
1. `username, err := deriveSSOUsername(ctx, s.Store, userInfo)` — from T7. `codes.Internal` on error.
2. Generate random password + bcrypt hash (unchanged from current lines 160–168).
3. `user, err := s.Store.CreateUser(ctx, &store.User{Username: username, Role: store.RoleUser, Nickname: userInfo.DisplayName, Email: userInfo.Email, AvatarURL: userInfo.AvatarURL, PasswordHash: string(passwordHash)})`.
4. `_, err := s.Store.CreateUserIdentity(ctx, &store.UserIdentity{UserID: user.ID, Provider: provider, ExternUID: externUID})`.
5. **Race recovery**: if `CreateUserIdentity` returns an error whose message matches one of the known unique-constraint markers (`strings.Contains(err.Error(), "UNIQUE constraint failed")`, `"duplicate key"`, `"Duplicate entry")` — reusing the same pattern as `server/router/api/v1/memo_service.go:103–105`):
- `_ = s.Store.DeleteUser(ctx, &store.DeleteUser{ID: user.ID})` (best-effort cleanup of the provisional local user).
- Re-read the winning `user_identity` via `s.Store.GetUserIdentity(ctx, &FindUserIdentity{Provider: &provider, ExternUID: &externUID})`; if still nil, return `codes.Internal` (should not happen under correct semantics).
- Load its user via `s.Store.GetUser(ctx, &FindUser{ID: &winner.UserID})`; set `existingUser`.
6. On any other `CreateUserIdentity` error: best-effort `DeleteUser` cleanup, then return `codes.Internal`.
7. On full success: set `existingUser = user`.
4. Leave the remainder of `SignIn` (row-status check, `doSignIn`, response construction) untouched.
**Boundaries**: Must NOT touch the password-credentials branch; must NOT modify `identifier_filter` logic; must NOT touch `doSignIn`, `SignOut`, or `RefreshToken`; must NOT add new fields to `SignInRequest`/`SignInResponse`.
**Dependencies**: T2, T3, T6 minimum for SQLite confidence; T7 for the derivation helper.
**Expected Outcome**:
- Sign-in with an IdP-issued identifier that fails `base.UIDMatcher` (e.g., `jane@example.com`) succeeds: a `user_identity` row is created, and the local `User.Username` is a derived valid username.
- Repeat sign-in for the same `(provider, extern_uid)` pair loads the same user by linkage, not by username.
- Two IdPs emitting the same `extern_uid` can each link to their own local users without colliding (G2).
**Validation**:
- `go build ./...` — expects PASS.
- `go vet ./...` — expects PASS.
- `go test ./store/test/ -run TestUserIdentity -count=1` — expects PASS (T6 regression check; ensures no store-layer drift).
## Out-of-Scope Tasks
The following are explicitly deferred per `definition.md` / `design.md` and will NOT be attempted during this execution:
- UI or API surfaces for linking/unlinking external identities.
- Update or delete paths for `user_identity` rows.
- Backfill / migration of existing users whose current `Username` matches an IdP identifier.
- Non-OAUTH2 IdP types.
- Protobuf or API changes to `SignInRequest`/`SignInResponse`.
- Adding foreign keys between `user_identity.provider` and `idp.uid`.
- Running PostgreSQL or MySQL integration tests locally (validation commands only cover SQLite, which is the default `DRIVER` in `store/test/store.go`).
...@@ -90,6 +90,33 @@ service UserService { ...@@ -90,6 +90,33 @@ service UserService {
option (google.api.method_signature) = "parent"; option (google.api.method_signature) = "parent";
} }
// ListLinkedIdentities returns a list of linked SSO identities for a user.
rpc ListLinkedIdentities(ListLinkedIdentitiesRequest) returns (ListLinkedIdentitiesResponse) {
option (google.api.http) = {get: "/api/v1/{parent=users/*}/linkedIdentities"};
option (google.api.method_signature) = "parent";
}
// CreateLinkedIdentity links an SSO identity to the authenticated user.
rpc CreateLinkedIdentity(CreateLinkedIdentityRequest) returns (LinkedIdentity) {
option (google.api.http) = {
post: "/api/v1/{parent=users/*}/linkedIdentities"
body: "*"
};
option (google.api.method_signature) = "parent,idp_name";
}
// GetLinkedIdentity gets a linked SSO identity for a user.
rpc GetLinkedIdentity(GetLinkedIdentityRequest) returns (LinkedIdentity) {
option (google.api.http) = {get: "/api/v1/{name=users/*/linkedIdentities/*}"};
option (google.api.method_signature) = "name";
}
// DeleteLinkedIdentity unlinks an SSO identity from a user.
rpc DeleteLinkedIdentity(DeleteLinkedIdentityRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {delete: "/api/v1/{name=users/*/linkedIdentities/*}"};
option (google.api.method_signature) = "name";
}
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user. // ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens. // PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) { rpc ListPersonalAccessTokens(ListPersonalAccessTokensRequest) returns (ListPersonalAccessTokensResponse) {
...@@ -466,6 +493,87 @@ message ListUserSettingsResponse { ...@@ -466,6 +493,87 @@ message ListUserSettingsResponse {
int32 total_size = 3; int32 total_size = 3;
} }
// LinkedIdentity represents an SSO identity linked to a user account.
message LinkedIdentity {
option (google.api.resource) = {
type: "memos.api.v1/LinkedIdentity"
pattern: "users/{user}/linkedIdentities/{linked_identity}"
singular: "linkedIdentity"
plural: "linkedIdentities"
};
// The resource name of the linked identity.
// Format: users/{user}/linkedIdentities/{linked_identity}
string name = 1 [(google.api.field_behavior) = IDENTIFIER];
// The resource name of the identity provider.
// Format: identity-providers/{uid}
string idp_name = 2 [
(google.api.field_behavior) = OUTPUT_ONLY,
(google.api.resource_reference) = {type: "memos.api.v1/IdentityProvider"}
];
// The external user identifier from the identity provider.
string extern_uid = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
}
message ListLinkedIdentitiesRequest {
// Required. The parent resource whose linked identities will be listed.
// Format: users/{user}
string parent = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/User"}
];
}
message ListLinkedIdentitiesResponse {
// The list of linked identities.
repeated LinkedIdentity linked_identities = 1;
}
message CreateLinkedIdentityRequest {
// Required. The parent user who owns the linked identity.
// Format: users/{user}
string parent = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/User"}
];
// Required. The identity provider to link.
// Format: identity-providers/{uid}
string idp_name = 2 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/IdentityProvider"}
];
// Required. The authorization code from the identity provider.
string code = 3 [(google.api.field_behavior) = REQUIRED];
// Required. The redirect URI used in the OAuth flow.
string redirect_uri = 4 [(google.api.field_behavior) = REQUIRED];
// Optional. The PKCE code verifier used in the OAuth flow.
string code_verifier = 5 [(google.api.field_behavior) = OPTIONAL];
}
message GetLinkedIdentityRequest {
// Required. The resource name of the linked identity to get.
// Format: users/{user}/linkedIdentities/{linked_identity}
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/LinkedIdentity"}
];
}
message DeleteLinkedIdentityRequest {
// Required. The resource name of the linked identity to delete.
// Format: users/{user}/linkedIdentities/{linked_identity}
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/LinkedIdentity"}
];
}
// PersonalAccessToken represents a long-lived token for API/script access. // PersonalAccessToken represents a long-lived token for API/script access.
// PATs are distinct from short-lived JWT access tokens used for session authentication. // PATs are distinct from short-lived JWT access tokens used for session authentication.
message PersonalAccessToken { message PersonalAccessToken {
......
...@@ -62,6 +62,18 @@ const ( ...@@ -62,6 +62,18 @@ const (
// UserServiceListUserSettingsProcedure is the fully-qualified name of the UserService's // UserServiceListUserSettingsProcedure is the fully-qualified name of the UserService's
// ListUserSettings RPC. // ListUserSettings RPC.
UserServiceListUserSettingsProcedure = "/memos.api.v1.UserService/ListUserSettings" UserServiceListUserSettingsProcedure = "/memos.api.v1.UserService/ListUserSettings"
// UserServiceListLinkedIdentitiesProcedure is the fully-qualified name of the UserService's
// ListLinkedIdentities RPC.
UserServiceListLinkedIdentitiesProcedure = "/memos.api.v1.UserService/ListLinkedIdentities"
// UserServiceCreateLinkedIdentityProcedure is the fully-qualified name of the UserService's
// CreateLinkedIdentity RPC.
UserServiceCreateLinkedIdentityProcedure = "/memos.api.v1.UserService/CreateLinkedIdentity"
// UserServiceGetLinkedIdentityProcedure is the fully-qualified name of the UserService's
// GetLinkedIdentity RPC.
UserServiceGetLinkedIdentityProcedure = "/memos.api.v1.UserService/GetLinkedIdentity"
// UserServiceDeleteLinkedIdentityProcedure is the fully-qualified name of the UserService's
// DeleteLinkedIdentity RPC.
UserServiceDeleteLinkedIdentityProcedure = "/memos.api.v1.UserService/DeleteLinkedIdentity"
// UserServiceListPersonalAccessTokensProcedure is the fully-qualified name of the UserService's // UserServiceListPersonalAccessTokensProcedure is the fully-qualified name of the UserService's
// ListPersonalAccessTokens RPC. // ListPersonalAccessTokens RPC.
UserServiceListPersonalAccessTokensProcedure = "/memos.api.v1.UserService/ListPersonalAccessTokens" UserServiceListPersonalAccessTokensProcedure = "/memos.api.v1.UserService/ListPersonalAccessTokens"
...@@ -119,6 +131,14 @@ type UserServiceClient interface { ...@@ -119,6 +131,14 @@ type UserServiceClient interface {
UpdateUserSetting(context.Context, *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error) UpdateUserSetting(context.Context, *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error)
// ListUserSettings returns a list of user settings. // ListUserSettings returns a list of user settings.
ListUserSettings(context.Context, *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error) ListUserSettings(context.Context, *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error)
// ListLinkedIdentities returns a list of linked SSO identities for a user.
ListLinkedIdentities(context.Context, *connect.Request[v1.ListLinkedIdentitiesRequest]) (*connect.Response[v1.ListLinkedIdentitiesResponse], error)
// CreateLinkedIdentity links an SSO identity to the authenticated user.
CreateLinkedIdentity(context.Context, *connect.Request[v1.CreateLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error)
// GetLinkedIdentity gets a linked SSO identity for a user.
GetLinkedIdentity(context.Context, *connect.Request[v1.GetLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error)
// DeleteLinkedIdentity unlinks an SSO identity from a user.
DeleteLinkedIdentity(context.Context, *connect.Request[v1.DeleteLinkedIdentityRequest]) (*connect.Response[emptypb.Empty], error)
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user. // ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens. // PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error)
...@@ -220,6 +240,30 @@ func NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts .. ...@@ -220,6 +240,30 @@ func NewUserServiceClient(httpClient connect.HTTPClient, baseURL string, opts ..
connect.WithSchema(userServiceMethods.ByName("ListUserSettings")), connect.WithSchema(userServiceMethods.ByName("ListUserSettings")),
connect.WithClientOptions(opts...), connect.WithClientOptions(opts...),
), ),
listLinkedIdentities: connect.NewClient[v1.ListLinkedIdentitiesRequest, v1.ListLinkedIdentitiesResponse](
httpClient,
baseURL+UserServiceListLinkedIdentitiesProcedure,
connect.WithSchema(userServiceMethods.ByName("ListLinkedIdentities")),
connect.WithClientOptions(opts...),
),
createLinkedIdentity: connect.NewClient[v1.CreateLinkedIdentityRequest, v1.LinkedIdentity](
httpClient,
baseURL+UserServiceCreateLinkedIdentityProcedure,
connect.WithSchema(userServiceMethods.ByName("CreateLinkedIdentity")),
connect.WithClientOptions(opts...),
),
getLinkedIdentity: connect.NewClient[v1.GetLinkedIdentityRequest, v1.LinkedIdentity](
httpClient,
baseURL+UserServiceGetLinkedIdentityProcedure,
connect.WithSchema(userServiceMethods.ByName("GetLinkedIdentity")),
connect.WithClientOptions(opts...),
),
deleteLinkedIdentity: connect.NewClient[v1.DeleteLinkedIdentityRequest, emptypb.Empty](
httpClient,
baseURL+UserServiceDeleteLinkedIdentityProcedure,
connect.WithSchema(userServiceMethods.ByName("DeleteLinkedIdentity")),
connect.WithClientOptions(opts...),
),
listPersonalAccessTokens: connect.NewClient[v1.ListPersonalAccessTokensRequest, v1.ListPersonalAccessTokensResponse]( listPersonalAccessTokens: connect.NewClient[v1.ListPersonalAccessTokensRequest, v1.ListPersonalAccessTokensResponse](
httpClient, httpClient,
baseURL+UserServiceListPersonalAccessTokensProcedure, baseURL+UserServiceListPersonalAccessTokensProcedure,
...@@ -296,6 +340,10 @@ type userServiceClient struct { ...@@ -296,6 +340,10 @@ type userServiceClient struct {
getUserSetting *connect.Client[v1.GetUserSettingRequest, v1.UserSetting] getUserSetting *connect.Client[v1.GetUserSettingRequest, v1.UserSetting]
updateUserSetting *connect.Client[v1.UpdateUserSettingRequest, v1.UserSetting] updateUserSetting *connect.Client[v1.UpdateUserSettingRequest, v1.UserSetting]
listUserSettings *connect.Client[v1.ListUserSettingsRequest, v1.ListUserSettingsResponse] listUserSettings *connect.Client[v1.ListUserSettingsRequest, v1.ListUserSettingsResponse]
listLinkedIdentities *connect.Client[v1.ListLinkedIdentitiesRequest, v1.ListLinkedIdentitiesResponse]
createLinkedIdentity *connect.Client[v1.CreateLinkedIdentityRequest, v1.LinkedIdentity]
getLinkedIdentity *connect.Client[v1.GetLinkedIdentityRequest, v1.LinkedIdentity]
deleteLinkedIdentity *connect.Client[v1.DeleteLinkedIdentityRequest, emptypb.Empty]
listPersonalAccessTokens *connect.Client[v1.ListPersonalAccessTokensRequest, v1.ListPersonalAccessTokensResponse] listPersonalAccessTokens *connect.Client[v1.ListPersonalAccessTokensRequest, v1.ListPersonalAccessTokensResponse]
createPersonalAccessToken *connect.Client[v1.CreatePersonalAccessTokenRequest, v1.CreatePersonalAccessTokenResponse] createPersonalAccessToken *connect.Client[v1.CreatePersonalAccessTokenRequest, v1.CreatePersonalAccessTokenResponse]
deletePersonalAccessToken *connect.Client[v1.DeletePersonalAccessTokenRequest, emptypb.Empty] deletePersonalAccessToken *connect.Client[v1.DeletePersonalAccessTokenRequest, emptypb.Empty]
...@@ -363,6 +411,26 @@ func (c *userServiceClient) ListUserSettings(ctx context.Context, req *connect.R ...@@ -363,6 +411,26 @@ func (c *userServiceClient) ListUserSettings(ctx context.Context, req *connect.R
return c.listUserSettings.CallUnary(ctx, req) return c.listUserSettings.CallUnary(ctx, req)
} }
// ListLinkedIdentities calls memos.api.v1.UserService.ListLinkedIdentities.
func (c *userServiceClient) ListLinkedIdentities(ctx context.Context, req *connect.Request[v1.ListLinkedIdentitiesRequest]) (*connect.Response[v1.ListLinkedIdentitiesResponse], error) {
return c.listLinkedIdentities.CallUnary(ctx, req)
}
// CreateLinkedIdentity calls memos.api.v1.UserService.CreateLinkedIdentity.
func (c *userServiceClient) CreateLinkedIdentity(ctx context.Context, req *connect.Request[v1.CreateLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error) {
return c.createLinkedIdentity.CallUnary(ctx, req)
}
// GetLinkedIdentity calls memos.api.v1.UserService.GetLinkedIdentity.
func (c *userServiceClient) GetLinkedIdentity(ctx context.Context, req *connect.Request[v1.GetLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error) {
return c.getLinkedIdentity.CallUnary(ctx, req)
}
// DeleteLinkedIdentity calls memos.api.v1.UserService.DeleteLinkedIdentity.
func (c *userServiceClient) DeleteLinkedIdentity(ctx context.Context, req *connect.Request[v1.DeleteLinkedIdentityRequest]) (*connect.Response[emptypb.Empty], error) {
return c.deleteLinkedIdentity.CallUnary(ctx, req)
}
// ListPersonalAccessTokens calls memos.api.v1.UserService.ListPersonalAccessTokens. // ListPersonalAccessTokens calls memos.api.v1.UserService.ListPersonalAccessTokens.
func (c *userServiceClient) ListPersonalAccessTokens(ctx context.Context, req *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) { func (c *userServiceClient) ListPersonalAccessTokens(ctx context.Context, req *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) {
return c.listPersonalAccessTokens.CallUnary(ctx, req) return c.listPersonalAccessTokens.CallUnary(ctx, req)
...@@ -438,6 +506,14 @@ type UserServiceHandler interface { ...@@ -438,6 +506,14 @@ type UserServiceHandler interface {
UpdateUserSetting(context.Context, *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error) UpdateUserSetting(context.Context, *connect.Request[v1.UpdateUserSettingRequest]) (*connect.Response[v1.UserSetting], error)
// ListUserSettings returns a list of user settings. // ListUserSettings returns a list of user settings.
ListUserSettings(context.Context, *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error) ListUserSettings(context.Context, *connect.Request[v1.ListUserSettingsRequest]) (*connect.Response[v1.ListUserSettingsResponse], error)
// ListLinkedIdentities returns a list of linked SSO identities for a user.
ListLinkedIdentities(context.Context, *connect.Request[v1.ListLinkedIdentitiesRequest]) (*connect.Response[v1.ListLinkedIdentitiesResponse], error)
// CreateLinkedIdentity links an SSO identity to the authenticated user.
CreateLinkedIdentity(context.Context, *connect.Request[v1.CreateLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error)
// GetLinkedIdentity gets a linked SSO identity for a user.
GetLinkedIdentity(context.Context, *connect.Request[v1.GetLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error)
// DeleteLinkedIdentity unlinks an SSO identity from a user.
DeleteLinkedIdentity(context.Context, *connect.Request[v1.DeleteLinkedIdentityRequest]) (*connect.Response[emptypb.Empty], error)
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user. // ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens. // PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error)
...@@ -535,6 +611,30 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption ...@@ -535,6 +611,30 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption
connect.WithSchema(userServiceMethods.ByName("ListUserSettings")), connect.WithSchema(userServiceMethods.ByName("ListUserSettings")),
connect.WithHandlerOptions(opts...), connect.WithHandlerOptions(opts...),
) )
userServiceListLinkedIdentitiesHandler := connect.NewUnaryHandler(
UserServiceListLinkedIdentitiesProcedure,
svc.ListLinkedIdentities,
connect.WithSchema(userServiceMethods.ByName("ListLinkedIdentities")),
connect.WithHandlerOptions(opts...),
)
userServiceCreateLinkedIdentityHandler := connect.NewUnaryHandler(
UserServiceCreateLinkedIdentityProcedure,
svc.CreateLinkedIdentity,
connect.WithSchema(userServiceMethods.ByName("CreateLinkedIdentity")),
connect.WithHandlerOptions(opts...),
)
userServiceGetLinkedIdentityHandler := connect.NewUnaryHandler(
UserServiceGetLinkedIdentityProcedure,
svc.GetLinkedIdentity,
connect.WithSchema(userServiceMethods.ByName("GetLinkedIdentity")),
connect.WithHandlerOptions(opts...),
)
userServiceDeleteLinkedIdentityHandler := connect.NewUnaryHandler(
UserServiceDeleteLinkedIdentityProcedure,
svc.DeleteLinkedIdentity,
connect.WithSchema(userServiceMethods.ByName("DeleteLinkedIdentity")),
connect.WithHandlerOptions(opts...),
)
userServiceListPersonalAccessTokensHandler := connect.NewUnaryHandler( userServiceListPersonalAccessTokensHandler := connect.NewUnaryHandler(
UserServiceListPersonalAccessTokensProcedure, UserServiceListPersonalAccessTokensProcedure,
svc.ListPersonalAccessTokens, svc.ListPersonalAccessTokens,
...@@ -619,6 +719,14 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption ...@@ -619,6 +719,14 @@ func NewUserServiceHandler(svc UserServiceHandler, opts ...connect.HandlerOption
userServiceUpdateUserSettingHandler.ServeHTTP(w, r) userServiceUpdateUserSettingHandler.ServeHTTP(w, r)
case UserServiceListUserSettingsProcedure: case UserServiceListUserSettingsProcedure:
userServiceListUserSettingsHandler.ServeHTTP(w, r) userServiceListUserSettingsHandler.ServeHTTP(w, r)
case UserServiceListLinkedIdentitiesProcedure:
userServiceListLinkedIdentitiesHandler.ServeHTTP(w, r)
case UserServiceCreateLinkedIdentityProcedure:
userServiceCreateLinkedIdentityHandler.ServeHTTP(w, r)
case UserServiceGetLinkedIdentityProcedure:
userServiceGetLinkedIdentityHandler.ServeHTTP(w, r)
case UserServiceDeleteLinkedIdentityProcedure:
userServiceDeleteLinkedIdentityHandler.ServeHTTP(w, r)
case UserServiceListPersonalAccessTokensProcedure: case UserServiceListPersonalAccessTokensProcedure:
userServiceListPersonalAccessTokensHandler.ServeHTTP(w, r) userServiceListPersonalAccessTokensHandler.ServeHTTP(w, r)
case UserServiceCreatePersonalAccessTokenProcedure: case UserServiceCreatePersonalAccessTokenProcedure:
...@@ -692,6 +800,22 @@ func (UnimplementedUserServiceHandler) ListUserSettings(context.Context, *connec ...@@ -692,6 +800,22 @@ func (UnimplementedUserServiceHandler) ListUserSettings(context.Context, *connec
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListUserSettings is not implemented")) return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListUserSettings is not implemented"))
} }
func (UnimplementedUserServiceHandler) ListLinkedIdentities(context.Context, *connect.Request[v1.ListLinkedIdentitiesRequest]) (*connect.Response[v1.ListLinkedIdentitiesResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListLinkedIdentities is not implemented"))
}
func (UnimplementedUserServiceHandler) CreateLinkedIdentity(context.Context, *connect.Request[v1.CreateLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.CreateLinkedIdentity is not implemented"))
}
func (UnimplementedUserServiceHandler) GetLinkedIdentity(context.Context, *connect.Request[v1.GetLinkedIdentityRequest]) (*connect.Response[v1.LinkedIdentity], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.GetLinkedIdentity is not implemented"))
}
func (UnimplementedUserServiceHandler) DeleteLinkedIdentity(context.Context, *connect.Request[v1.DeleteLinkedIdentityRequest]) (*connect.Response[emptypb.Empty], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.DeleteLinkedIdentity is not implemented"))
}
func (UnimplementedUserServiceHandler) ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) { func (UnimplementedUserServiceHandler) ListPersonalAccessTokens(context.Context, *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListPersonalAccessTokens is not implemented")) return nil, connect.NewError(connect.CodeUnimplemented, errors.New("memos.api.v1.UserService.ListPersonalAccessTokens is not implemented"))
} }
......
...@@ -175,7 +175,7 @@ func (x UserNotification_Status) Number() protoreflect.EnumNumber { ...@@ -175,7 +175,7 @@ func (x UserNotification_Status) Number() protoreflect.EnumNumber {
// Deprecated: Use UserNotification_Status.Descriptor instead. // Deprecated: Use UserNotification_Status.Descriptor instead.
func (UserNotification_Status) EnumDescriptor() ([]byte, []int) { func (UserNotification_Status) EnumDescriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30, 0} return file_api_v1_user_service_proto_rawDescGZIP(), []int{36, 0}
} }
type UserNotification_Type int32 type UserNotification_Type int32
...@@ -224,7 +224,7 @@ func (x UserNotification_Type) Number() protoreflect.EnumNumber { ...@@ -224,7 +224,7 @@ func (x UserNotification_Type) Number() protoreflect.EnumNumber {
// Deprecated: Use UserNotification_Type.Descriptor instead. // Deprecated: Use UserNotification_Type.Descriptor instead.
func (UserNotification_Type) EnumDescriptor() ([]byte, []int) { func (UserNotification_Type) EnumDescriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30, 1} return file_api_v1_user_service_proto_rawDescGZIP(), []int{36, 1}
} }
type User struct { type User struct {
...@@ -1390,6 +1390,338 @@ func (x *ListUserSettingsResponse) GetTotalSize() int32 { ...@@ -1390,6 +1390,338 @@ func (x *ListUserSettingsResponse) GetTotalSize() int32 {
return 0 return 0
} }
// LinkedIdentity represents an SSO identity linked to a user account.
type LinkedIdentity struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The resource name of the linked identity.
// Format: users/{user}/linkedIdentities/{linked_identity}
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// The resource name of the identity provider.
// Format: identity-providers/{uid}
IdpName string `protobuf:"bytes,2,opt,name=idp_name,json=idpName,proto3" json:"idp_name,omitempty"`
// The external user identifier from the identity provider.
ExternUid string `protobuf:"bytes,3,opt,name=extern_uid,json=externUid,proto3" json:"extern_uid,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LinkedIdentity) Reset() {
*x = LinkedIdentity{}
mi := &file_api_v1_user_service_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LinkedIdentity) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LinkedIdentity) ProtoMessage() {}
func (x *LinkedIdentity) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LinkedIdentity.ProtoReflect.Descriptor instead.
func (*LinkedIdentity) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{18}
}
func (x *LinkedIdentity) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *LinkedIdentity) GetIdpName() string {
if x != nil {
return x.IdpName
}
return ""
}
func (x *LinkedIdentity) GetExternUid() string {
if x != nil {
return x.ExternUid
}
return ""
}
type ListLinkedIdentitiesRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The parent resource whose linked identities will be listed.
// Format: users/{user}
Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListLinkedIdentitiesRequest) Reset() {
*x = ListLinkedIdentitiesRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListLinkedIdentitiesRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListLinkedIdentitiesRequest) ProtoMessage() {}
func (x *ListLinkedIdentitiesRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListLinkedIdentitiesRequest.ProtoReflect.Descriptor instead.
func (*ListLinkedIdentitiesRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{19}
}
func (x *ListLinkedIdentitiesRequest) GetParent() string {
if x != nil {
return x.Parent
}
return ""
}
type ListLinkedIdentitiesResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The list of linked identities.
LinkedIdentities []*LinkedIdentity `protobuf:"bytes,1,rep,name=linked_identities,json=linkedIdentities,proto3" json:"linked_identities,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListLinkedIdentitiesResponse) Reset() {
*x = ListLinkedIdentitiesResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListLinkedIdentitiesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListLinkedIdentitiesResponse) ProtoMessage() {}
func (x *ListLinkedIdentitiesResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListLinkedIdentitiesResponse.ProtoReflect.Descriptor instead.
func (*ListLinkedIdentitiesResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{20}
}
func (x *ListLinkedIdentitiesResponse) GetLinkedIdentities() []*LinkedIdentity {
if x != nil {
return x.LinkedIdentities
}
return nil
}
type CreateLinkedIdentityRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The parent user who owns the linked identity.
// Format: users/{user}
Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"`
// Required. The identity provider to link.
// Format: identity-providers/{uid}
IdpName string `protobuf:"bytes,2,opt,name=idp_name,json=idpName,proto3" json:"idp_name,omitempty"`
// Required. The authorization code from the identity provider.
Code string `protobuf:"bytes,3,opt,name=code,proto3" json:"code,omitempty"`
// Required. The redirect URI used in the OAuth flow.
RedirectUri string `protobuf:"bytes,4,opt,name=redirect_uri,json=redirectUri,proto3" json:"redirect_uri,omitempty"`
// Optional. The PKCE code verifier used in the OAuth flow.
CodeVerifier string `protobuf:"bytes,5,opt,name=code_verifier,json=codeVerifier,proto3" json:"code_verifier,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateLinkedIdentityRequest) Reset() {
*x = CreateLinkedIdentityRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateLinkedIdentityRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateLinkedIdentityRequest) ProtoMessage() {}
func (x *CreateLinkedIdentityRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateLinkedIdentityRequest.ProtoReflect.Descriptor instead.
func (*CreateLinkedIdentityRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{21}
}
func (x *CreateLinkedIdentityRequest) GetParent() string {
if x != nil {
return x.Parent
}
return ""
}
func (x *CreateLinkedIdentityRequest) GetIdpName() string {
if x != nil {
return x.IdpName
}
return ""
}
func (x *CreateLinkedIdentityRequest) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
func (x *CreateLinkedIdentityRequest) GetRedirectUri() string {
if x != nil {
return x.RedirectUri
}
return ""
}
func (x *CreateLinkedIdentityRequest) GetCodeVerifier() string {
if x != nil {
return x.CodeVerifier
}
return ""
}
type GetLinkedIdentityRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The resource name of the linked identity to get.
// Format: users/{user}/linkedIdentities/{linked_identity}
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetLinkedIdentityRequest) Reset() {
*x = GetLinkedIdentityRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetLinkedIdentityRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetLinkedIdentityRequest) ProtoMessage() {}
func (x *GetLinkedIdentityRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetLinkedIdentityRequest.ProtoReflect.Descriptor instead.
func (*GetLinkedIdentityRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{22}
}
func (x *GetLinkedIdentityRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type DeleteLinkedIdentityRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The resource name of the linked identity to delete.
// Format: users/{user}/linkedIdentities/{linked_identity}
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteLinkedIdentityRequest) Reset() {
*x = DeleteLinkedIdentityRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteLinkedIdentityRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteLinkedIdentityRequest) ProtoMessage() {}
func (x *DeleteLinkedIdentityRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteLinkedIdentityRequest.ProtoReflect.Descriptor instead.
func (*DeleteLinkedIdentityRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{23}
}
func (x *DeleteLinkedIdentityRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
// PersonalAccessToken represents a long-lived token for API/script access. // PersonalAccessToken represents a long-lived token for API/script access.
// PATs are distinct from short-lived JWT access tokens used for session authentication. // PATs are distinct from short-lived JWT access tokens used for session authentication.
type PersonalAccessToken struct { type PersonalAccessToken struct {
...@@ -1411,7 +1743,7 @@ type PersonalAccessToken struct { ...@@ -1411,7 +1743,7 @@ type PersonalAccessToken struct {
func (x *PersonalAccessToken) Reset() { func (x *PersonalAccessToken) Reset() {
*x = PersonalAccessToken{} *x = PersonalAccessToken{}
mi := &file_api_v1_user_service_proto_msgTypes[18] mi := &file_api_v1_user_service_proto_msgTypes[24]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -1423,7 +1755,7 @@ func (x *PersonalAccessToken) String() string { ...@@ -1423,7 +1755,7 @@ func (x *PersonalAccessToken) String() string {
func (*PersonalAccessToken) ProtoMessage() {} func (*PersonalAccessToken) ProtoMessage() {}
func (x *PersonalAccessToken) ProtoReflect() protoreflect.Message { func (x *PersonalAccessToken) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[18] mi := &file_api_v1_user_service_proto_msgTypes[24]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -1436,7 +1768,7 @@ func (x *PersonalAccessToken) ProtoReflect() protoreflect.Message { ...@@ -1436,7 +1768,7 @@ func (x *PersonalAccessToken) ProtoReflect() protoreflect.Message {
// Deprecated: Use PersonalAccessToken.ProtoReflect.Descriptor instead. // Deprecated: Use PersonalAccessToken.ProtoReflect.Descriptor instead.
func (*PersonalAccessToken) Descriptor() ([]byte, []int) { func (*PersonalAccessToken) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{18} return file_api_v1_user_service_proto_rawDescGZIP(), []int{24}
} }
func (x *PersonalAccessToken) GetName() string { func (x *PersonalAccessToken) GetName() string {
...@@ -1489,7 +1821,7 @@ type ListPersonalAccessTokensRequest struct { ...@@ -1489,7 +1821,7 @@ type ListPersonalAccessTokensRequest struct {
func (x *ListPersonalAccessTokensRequest) Reset() { func (x *ListPersonalAccessTokensRequest) Reset() {
*x = ListPersonalAccessTokensRequest{} *x = ListPersonalAccessTokensRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[19] mi := &file_api_v1_user_service_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -1501,7 +1833,7 @@ func (x *ListPersonalAccessTokensRequest) String() string { ...@@ -1501,7 +1833,7 @@ func (x *ListPersonalAccessTokensRequest) String() string {
func (*ListPersonalAccessTokensRequest) ProtoMessage() {} func (*ListPersonalAccessTokensRequest) ProtoMessage() {}
func (x *ListPersonalAccessTokensRequest) ProtoReflect() protoreflect.Message { func (x *ListPersonalAccessTokensRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[19] mi := &file_api_v1_user_service_proto_msgTypes[25]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -1514,7 +1846,7 @@ func (x *ListPersonalAccessTokensRequest) ProtoReflect() protoreflect.Message { ...@@ -1514,7 +1846,7 @@ func (x *ListPersonalAccessTokensRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListPersonalAccessTokensRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ListPersonalAccessTokensRequest.ProtoReflect.Descriptor instead.
func (*ListPersonalAccessTokensRequest) Descriptor() ([]byte, []int) { func (*ListPersonalAccessTokensRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{19} return file_api_v1_user_service_proto_rawDescGZIP(), []int{25}
} }
func (x *ListPersonalAccessTokensRequest) GetParent() string { func (x *ListPersonalAccessTokensRequest) GetParent() string {
...@@ -1552,7 +1884,7 @@ type ListPersonalAccessTokensResponse struct { ...@@ -1552,7 +1884,7 @@ type ListPersonalAccessTokensResponse struct {
func (x *ListPersonalAccessTokensResponse) Reset() { func (x *ListPersonalAccessTokensResponse) Reset() {
*x = ListPersonalAccessTokensResponse{} *x = ListPersonalAccessTokensResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[20] mi := &file_api_v1_user_service_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -1564,7 +1896,7 @@ func (x *ListPersonalAccessTokensResponse) String() string { ...@@ -1564,7 +1896,7 @@ func (x *ListPersonalAccessTokensResponse) String() string {
func (*ListPersonalAccessTokensResponse) ProtoMessage() {} func (*ListPersonalAccessTokensResponse) ProtoMessage() {}
func (x *ListPersonalAccessTokensResponse) ProtoReflect() protoreflect.Message { func (x *ListPersonalAccessTokensResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[20] mi := &file_api_v1_user_service_proto_msgTypes[26]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -1577,7 +1909,7 @@ func (x *ListPersonalAccessTokensResponse) ProtoReflect() protoreflect.Message { ...@@ -1577,7 +1909,7 @@ func (x *ListPersonalAccessTokensResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListPersonalAccessTokensResponse.ProtoReflect.Descriptor instead. // Deprecated: Use ListPersonalAccessTokensResponse.ProtoReflect.Descriptor instead.
func (*ListPersonalAccessTokensResponse) Descriptor() ([]byte, []int) { func (*ListPersonalAccessTokensResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{20} return file_api_v1_user_service_proto_rawDescGZIP(), []int{26}
} }
func (x *ListPersonalAccessTokensResponse) GetPersonalAccessTokens() []*PersonalAccessToken { func (x *ListPersonalAccessTokensResponse) GetPersonalAccessTokens() []*PersonalAccessToken {
...@@ -1616,7 +1948,7 @@ type CreatePersonalAccessTokenRequest struct { ...@@ -1616,7 +1948,7 @@ type CreatePersonalAccessTokenRequest struct {
func (x *CreatePersonalAccessTokenRequest) Reset() { func (x *CreatePersonalAccessTokenRequest) Reset() {
*x = CreatePersonalAccessTokenRequest{} *x = CreatePersonalAccessTokenRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[21] mi := &file_api_v1_user_service_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -1628,7 +1960,7 @@ func (x *CreatePersonalAccessTokenRequest) String() string { ...@@ -1628,7 +1960,7 @@ func (x *CreatePersonalAccessTokenRequest) String() string {
func (*CreatePersonalAccessTokenRequest) ProtoMessage() {} func (*CreatePersonalAccessTokenRequest) ProtoMessage() {}
func (x *CreatePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message { func (x *CreatePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[21] mi := &file_api_v1_user_service_proto_msgTypes[27]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -1641,7 +1973,7 @@ func (x *CreatePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message { ...@@ -1641,7 +1973,7 @@ func (x *CreatePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreatePersonalAccessTokenRequest.ProtoReflect.Descriptor instead. // Deprecated: Use CreatePersonalAccessTokenRequest.ProtoReflect.Descriptor instead.
func (*CreatePersonalAccessTokenRequest) Descriptor() ([]byte, []int) { func (*CreatePersonalAccessTokenRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{21} return file_api_v1_user_service_proto_rawDescGZIP(), []int{27}
} }
func (x *CreatePersonalAccessTokenRequest) GetParent() string { func (x *CreatePersonalAccessTokenRequest) GetParent() string {
...@@ -1678,7 +2010,7 @@ type CreatePersonalAccessTokenResponse struct { ...@@ -1678,7 +2010,7 @@ type CreatePersonalAccessTokenResponse struct {
func (x *CreatePersonalAccessTokenResponse) Reset() { func (x *CreatePersonalAccessTokenResponse) Reset() {
*x = CreatePersonalAccessTokenResponse{} *x = CreatePersonalAccessTokenResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[22] mi := &file_api_v1_user_service_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -1690,7 +2022,7 @@ func (x *CreatePersonalAccessTokenResponse) String() string { ...@@ -1690,7 +2022,7 @@ func (x *CreatePersonalAccessTokenResponse) String() string {
func (*CreatePersonalAccessTokenResponse) ProtoMessage() {} func (*CreatePersonalAccessTokenResponse) ProtoMessage() {}
func (x *CreatePersonalAccessTokenResponse) ProtoReflect() protoreflect.Message { func (x *CreatePersonalAccessTokenResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[22] mi := &file_api_v1_user_service_proto_msgTypes[28]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -1703,7 +2035,7 @@ func (x *CreatePersonalAccessTokenResponse) ProtoReflect() protoreflect.Message ...@@ -1703,7 +2035,7 @@ func (x *CreatePersonalAccessTokenResponse) ProtoReflect() protoreflect.Message
// Deprecated: Use CreatePersonalAccessTokenResponse.ProtoReflect.Descriptor instead. // Deprecated: Use CreatePersonalAccessTokenResponse.ProtoReflect.Descriptor instead.
func (*CreatePersonalAccessTokenResponse) Descriptor() ([]byte, []int) { func (*CreatePersonalAccessTokenResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{22} return file_api_v1_user_service_proto_rawDescGZIP(), []int{28}
} }
func (x *CreatePersonalAccessTokenResponse) GetPersonalAccessToken() *PersonalAccessToken { func (x *CreatePersonalAccessTokenResponse) GetPersonalAccessToken() *PersonalAccessToken {
...@@ -1731,7 +2063,7 @@ type DeletePersonalAccessTokenRequest struct { ...@@ -1731,7 +2063,7 @@ type DeletePersonalAccessTokenRequest struct {
func (x *DeletePersonalAccessTokenRequest) Reset() { func (x *DeletePersonalAccessTokenRequest) Reset() {
*x = DeletePersonalAccessTokenRequest{} *x = DeletePersonalAccessTokenRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[23] mi := &file_api_v1_user_service_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -1743,7 +2075,7 @@ func (x *DeletePersonalAccessTokenRequest) String() string { ...@@ -1743,7 +2075,7 @@ func (x *DeletePersonalAccessTokenRequest) String() string {
func (*DeletePersonalAccessTokenRequest) ProtoMessage() {} func (*DeletePersonalAccessTokenRequest) ProtoMessage() {}
func (x *DeletePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message { func (x *DeletePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[23] mi := &file_api_v1_user_service_proto_msgTypes[29]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -1756,7 +2088,7 @@ func (x *DeletePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message { ...@@ -1756,7 +2088,7 @@ func (x *DeletePersonalAccessTokenRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeletePersonalAccessTokenRequest.ProtoReflect.Descriptor instead. // Deprecated: Use DeletePersonalAccessTokenRequest.ProtoReflect.Descriptor instead.
func (*DeletePersonalAccessTokenRequest) Descriptor() ([]byte, []int) { func (*DeletePersonalAccessTokenRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{23} return file_api_v1_user_service_proto_rawDescGZIP(), []int{29}
} }
func (x *DeletePersonalAccessTokenRequest) GetName() string { func (x *DeletePersonalAccessTokenRequest) GetName() string {
...@@ -1786,7 +2118,7 @@ type UserWebhook struct { ...@@ -1786,7 +2118,7 @@ type UserWebhook struct {
func (x *UserWebhook) Reset() { func (x *UserWebhook) Reset() {
*x = UserWebhook{} *x = UserWebhook{}
mi := &file_api_v1_user_service_proto_msgTypes[24] mi := &file_api_v1_user_service_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -1798,7 +2130,7 @@ func (x *UserWebhook) String() string { ...@@ -1798,7 +2130,7 @@ func (x *UserWebhook) String() string {
func (*UserWebhook) ProtoMessage() {} func (*UserWebhook) ProtoMessage() {}
func (x *UserWebhook) ProtoReflect() protoreflect.Message { func (x *UserWebhook) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[24] mi := &file_api_v1_user_service_proto_msgTypes[30]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -1811,7 +2143,7 @@ func (x *UserWebhook) ProtoReflect() protoreflect.Message { ...@@ -1811,7 +2143,7 @@ func (x *UserWebhook) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserWebhook.ProtoReflect.Descriptor instead. // Deprecated: Use UserWebhook.ProtoReflect.Descriptor instead.
func (*UserWebhook) Descriptor() ([]byte, []int) { func (*UserWebhook) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{24} return file_api_v1_user_service_proto_rawDescGZIP(), []int{30}
} }
func (x *UserWebhook) GetName() string { func (x *UserWebhook) GetName() string {
...@@ -1860,7 +2192,7 @@ type ListUserWebhooksRequest struct { ...@@ -1860,7 +2192,7 @@ type ListUserWebhooksRequest struct {
func (x *ListUserWebhooksRequest) Reset() { func (x *ListUserWebhooksRequest) Reset() {
*x = ListUserWebhooksRequest{} *x = ListUserWebhooksRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[25] mi := &file_api_v1_user_service_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -1872,7 +2204,7 @@ func (x *ListUserWebhooksRequest) String() string { ...@@ -1872,7 +2204,7 @@ func (x *ListUserWebhooksRequest) String() string {
func (*ListUserWebhooksRequest) ProtoMessage() {} func (*ListUserWebhooksRequest) ProtoMessage() {}
func (x *ListUserWebhooksRequest) ProtoReflect() protoreflect.Message { func (x *ListUserWebhooksRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[25] mi := &file_api_v1_user_service_proto_msgTypes[31]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -1885,7 +2217,7 @@ func (x *ListUserWebhooksRequest) ProtoReflect() protoreflect.Message { ...@@ -1885,7 +2217,7 @@ func (x *ListUserWebhooksRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUserWebhooksRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ListUserWebhooksRequest.ProtoReflect.Descriptor instead.
func (*ListUserWebhooksRequest) Descriptor() ([]byte, []int) { func (*ListUserWebhooksRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{25} return file_api_v1_user_service_proto_rawDescGZIP(), []int{31}
} }
func (x *ListUserWebhooksRequest) GetParent() string { func (x *ListUserWebhooksRequest) GetParent() string {
...@@ -1905,7 +2237,7 @@ type ListUserWebhooksResponse struct { ...@@ -1905,7 +2237,7 @@ type ListUserWebhooksResponse struct {
func (x *ListUserWebhooksResponse) Reset() { func (x *ListUserWebhooksResponse) Reset() {
*x = ListUserWebhooksResponse{} *x = ListUserWebhooksResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[26] mi := &file_api_v1_user_service_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -1917,7 +2249,7 @@ func (x *ListUserWebhooksResponse) String() string { ...@@ -1917,7 +2249,7 @@ func (x *ListUserWebhooksResponse) String() string {
func (*ListUserWebhooksResponse) ProtoMessage() {} func (*ListUserWebhooksResponse) ProtoMessage() {}
func (x *ListUserWebhooksResponse) ProtoReflect() protoreflect.Message { func (x *ListUserWebhooksResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[26] mi := &file_api_v1_user_service_proto_msgTypes[32]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -1930,7 +2262,7 @@ func (x *ListUserWebhooksResponse) ProtoReflect() protoreflect.Message { ...@@ -1930,7 +2262,7 @@ func (x *ListUserWebhooksResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUserWebhooksResponse.ProtoReflect.Descriptor instead. // Deprecated: Use ListUserWebhooksResponse.ProtoReflect.Descriptor instead.
func (*ListUserWebhooksResponse) Descriptor() ([]byte, []int) { func (*ListUserWebhooksResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{26} return file_api_v1_user_service_proto_rawDescGZIP(), []int{32}
} }
func (x *ListUserWebhooksResponse) GetWebhooks() []*UserWebhook { func (x *ListUserWebhooksResponse) GetWebhooks() []*UserWebhook {
...@@ -1953,7 +2285,7 @@ type CreateUserWebhookRequest struct { ...@@ -1953,7 +2285,7 @@ type CreateUserWebhookRequest struct {
func (x *CreateUserWebhookRequest) Reset() { func (x *CreateUserWebhookRequest) Reset() {
*x = CreateUserWebhookRequest{} *x = CreateUserWebhookRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[27] mi := &file_api_v1_user_service_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -1965,7 +2297,7 @@ func (x *CreateUserWebhookRequest) String() string { ...@@ -1965,7 +2297,7 @@ func (x *CreateUserWebhookRequest) String() string {
func (*CreateUserWebhookRequest) ProtoMessage() {} func (*CreateUserWebhookRequest) ProtoMessage() {}
func (x *CreateUserWebhookRequest) ProtoReflect() protoreflect.Message { func (x *CreateUserWebhookRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[27] mi := &file_api_v1_user_service_proto_msgTypes[33]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -1978,7 +2310,7 @@ func (x *CreateUserWebhookRequest) ProtoReflect() protoreflect.Message { ...@@ -1978,7 +2310,7 @@ func (x *CreateUserWebhookRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateUserWebhookRequest.ProtoReflect.Descriptor instead. // Deprecated: Use CreateUserWebhookRequest.ProtoReflect.Descriptor instead.
func (*CreateUserWebhookRequest) Descriptor() ([]byte, []int) { func (*CreateUserWebhookRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{27} return file_api_v1_user_service_proto_rawDescGZIP(), []int{33}
} }
func (x *CreateUserWebhookRequest) GetParent() string { func (x *CreateUserWebhookRequest) GetParent() string {
...@@ -2007,7 +2339,7 @@ type UpdateUserWebhookRequest struct { ...@@ -2007,7 +2339,7 @@ type UpdateUserWebhookRequest struct {
func (x *UpdateUserWebhookRequest) Reset() { func (x *UpdateUserWebhookRequest) Reset() {
*x = UpdateUserWebhookRequest{} *x = UpdateUserWebhookRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[28] mi := &file_api_v1_user_service_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -2019,7 +2351,7 @@ func (x *UpdateUserWebhookRequest) String() string { ...@@ -2019,7 +2351,7 @@ func (x *UpdateUserWebhookRequest) String() string {
func (*UpdateUserWebhookRequest) ProtoMessage() {} func (*UpdateUserWebhookRequest) ProtoMessage() {}
func (x *UpdateUserWebhookRequest) ProtoReflect() protoreflect.Message { func (x *UpdateUserWebhookRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[28] mi := &file_api_v1_user_service_proto_msgTypes[34]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -2032,7 +2364,7 @@ func (x *UpdateUserWebhookRequest) ProtoReflect() protoreflect.Message { ...@@ -2032,7 +2364,7 @@ func (x *UpdateUserWebhookRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateUserWebhookRequest.ProtoReflect.Descriptor instead. // Deprecated: Use UpdateUserWebhookRequest.ProtoReflect.Descriptor instead.
func (*UpdateUserWebhookRequest) Descriptor() ([]byte, []int) { func (*UpdateUserWebhookRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{28} return file_api_v1_user_service_proto_rawDescGZIP(), []int{34}
} }
func (x *UpdateUserWebhookRequest) GetWebhook() *UserWebhook { func (x *UpdateUserWebhookRequest) GetWebhook() *UserWebhook {
...@@ -2060,7 +2392,7 @@ type DeleteUserWebhookRequest struct { ...@@ -2060,7 +2392,7 @@ type DeleteUserWebhookRequest struct {
func (x *DeleteUserWebhookRequest) Reset() { func (x *DeleteUserWebhookRequest) Reset() {
*x = DeleteUserWebhookRequest{} *x = DeleteUserWebhookRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[29] mi := &file_api_v1_user_service_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -2072,7 +2404,7 @@ func (x *DeleteUserWebhookRequest) String() string { ...@@ -2072,7 +2404,7 @@ func (x *DeleteUserWebhookRequest) String() string {
func (*DeleteUserWebhookRequest) ProtoMessage() {} func (*DeleteUserWebhookRequest) ProtoMessage() {}
func (x *DeleteUserWebhookRequest) ProtoReflect() protoreflect.Message { func (x *DeleteUserWebhookRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[29] mi := &file_api_v1_user_service_proto_msgTypes[35]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -2085,7 +2417,7 @@ func (x *DeleteUserWebhookRequest) ProtoReflect() protoreflect.Message { ...@@ -2085,7 +2417,7 @@ func (x *DeleteUserWebhookRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeleteUserWebhookRequest.ProtoReflect.Descriptor instead. // Deprecated: Use DeleteUserWebhookRequest.ProtoReflect.Descriptor instead.
func (*DeleteUserWebhookRequest) Descriptor() ([]byte, []int) { func (*DeleteUserWebhookRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{29} return file_api_v1_user_service_proto_rawDescGZIP(), []int{35}
} }
func (x *DeleteUserWebhookRequest) GetName() string { func (x *DeleteUserWebhookRequest) GetName() string {
...@@ -2122,7 +2454,7 @@ type UserNotification struct { ...@@ -2122,7 +2454,7 @@ type UserNotification struct {
func (x *UserNotification) Reset() { func (x *UserNotification) Reset() {
*x = UserNotification{} *x = UserNotification{}
mi := &file_api_v1_user_service_proto_msgTypes[30] mi := &file_api_v1_user_service_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -2134,7 +2466,7 @@ func (x *UserNotification) String() string { ...@@ -2134,7 +2466,7 @@ func (x *UserNotification) String() string {
func (*UserNotification) ProtoMessage() {} func (*UserNotification) ProtoMessage() {}
func (x *UserNotification) ProtoReflect() protoreflect.Message { func (x *UserNotification) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[30] mi := &file_api_v1_user_service_proto_msgTypes[36]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -2147,7 +2479,7 @@ func (x *UserNotification) ProtoReflect() protoreflect.Message { ...@@ -2147,7 +2479,7 @@ func (x *UserNotification) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserNotification.ProtoReflect.Descriptor instead. // Deprecated: Use UserNotification.ProtoReflect.Descriptor instead.
func (*UserNotification) Descriptor() ([]byte, []int) { func (*UserNotification) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30} return file_api_v1_user_service_proto_rawDescGZIP(), []int{36}
} }
func (x *UserNotification) GetName() string { func (x *UserNotification) GetName() string {
...@@ -2247,7 +2579,7 @@ type ListUserNotificationsRequest struct { ...@@ -2247,7 +2579,7 @@ type ListUserNotificationsRequest struct {
func (x *ListUserNotificationsRequest) Reset() { func (x *ListUserNotificationsRequest) Reset() {
*x = ListUserNotificationsRequest{} *x = ListUserNotificationsRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[31] mi := &file_api_v1_user_service_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -2259,7 +2591,7 @@ func (x *ListUserNotificationsRequest) String() string { ...@@ -2259,7 +2591,7 @@ func (x *ListUserNotificationsRequest) String() string {
func (*ListUserNotificationsRequest) ProtoMessage() {} func (*ListUserNotificationsRequest) ProtoMessage() {}
func (x *ListUserNotificationsRequest) ProtoReflect() protoreflect.Message { func (x *ListUserNotificationsRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[31] mi := &file_api_v1_user_service_proto_msgTypes[37]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -2272,7 +2604,7 @@ func (x *ListUserNotificationsRequest) ProtoReflect() protoreflect.Message { ...@@ -2272,7 +2604,7 @@ func (x *ListUserNotificationsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUserNotificationsRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ListUserNotificationsRequest.ProtoReflect.Descriptor instead.
func (*ListUserNotificationsRequest) Descriptor() ([]byte, []int) { func (*ListUserNotificationsRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{31} return file_api_v1_user_service_proto_rawDescGZIP(), []int{37}
} }
func (x *ListUserNotificationsRequest) GetParent() string { func (x *ListUserNotificationsRequest) GetParent() string {
...@@ -2313,7 +2645,7 @@ type ListUserNotificationsResponse struct { ...@@ -2313,7 +2645,7 @@ type ListUserNotificationsResponse struct {
func (x *ListUserNotificationsResponse) Reset() { func (x *ListUserNotificationsResponse) Reset() {
*x = ListUserNotificationsResponse{} *x = ListUserNotificationsResponse{}
mi := &file_api_v1_user_service_proto_msgTypes[32] mi := &file_api_v1_user_service_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -2325,7 +2657,7 @@ func (x *ListUserNotificationsResponse) String() string { ...@@ -2325,7 +2657,7 @@ func (x *ListUserNotificationsResponse) String() string {
func (*ListUserNotificationsResponse) ProtoMessage() {} func (*ListUserNotificationsResponse) ProtoMessage() {}
func (x *ListUserNotificationsResponse) ProtoReflect() protoreflect.Message { func (x *ListUserNotificationsResponse) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[32] mi := &file_api_v1_user_service_proto_msgTypes[38]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -2338,7 +2670,7 @@ func (x *ListUserNotificationsResponse) ProtoReflect() protoreflect.Message { ...@@ -2338,7 +2670,7 @@ func (x *ListUserNotificationsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListUserNotificationsResponse.ProtoReflect.Descriptor instead. // Deprecated: Use ListUserNotificationsResponse.ProtoReflect.Descriptor instead.
func (*ListUserNotificationsResponse) Descriptor() ([]byte, []int) { func (*ListUserNotificationsResponse) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{32} return file_api_v1_user_service_proto_rawDescGZIP(), []int{38}
} }
func (x *ListUserNotificationsResponse) GetNotifications() []*UserNotification { func (x *ListUserNotificationsResponse) GetNotifications() []*UserNotification {
...@@ -2365,7 +2697,7 @@ type UpdateUserNotificationRequest struct { ...@@ -2365,7 +2697,7 @@ type UpdateUserNotificationRequest struct {
func (x *UpdateUserNotificationRequest) Reset() { func (x *UpdateUserNotificationRequest) Reset() {
*x = UpdateUserNotificationRequest{} *x = UpdateUserNotificationRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[33] mi := &file_api_v1_user_service_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -2377,7 +2709,7 @@ func (x *UpdateUserNotificationRequest) String() string { ...@@ -2377,7 +2709,7 @@ func (x *UpdateUserNotificationRequest) String() string {
func (*UpdateUserNotificationRequest) ProtoMessage() {} func (*UpdateUserNotificationRequest) ProtoMessage() {}
func (x *UpdateUserNotificationRequest) ProtoReflect() protoreflect.Message { func (x *UpdateUserNotificationRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[33] mi := &file_api_v1_user_service_proto_msgTypes[39]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -2390,7 +2722,7 @@ func (x *UpdateUserNotificationRequest) ProtoReflect() protoreflect.Message { ...@@ -2390,7 +2722,7 @@ func (x *UpdateUserNotificationRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateUserNotificationRequest.ProtoReflect.Descriptor instead. // Deprecated: Use UpdateUserNotificationRequest.ProtoReflect.Descriptor instead.
func (*UpdateUserNotificationRequest) Descriptor() ([]byte, []int) { func (*UpdateUserNotificationRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{33} return file_api_v1_user_service_proto_rawDescGZIP(), []int{39}
} }
func (x *UpdateUserNotificationRequest) GetNotification() *UserNotification { func (x *UpdateUserNotificationRequest) GetNotification() *UserNotification {
...@@ -2417,7 +2749,7 @@ type DeleteUserNotificationRequest struct { ...@@ -2417,7 +2749,7 @@ type DeleteUserNotificationRequest struct {
func (x *DeleteUserNotificationRequest) Reset() { func (x *DeleteUserNotificationRequest) Reset() {
*x = DeleteUserNotificationRequest{} *x = DeleteUserNotificationRequest{}
mi := &file_api_v1_user_service_proto_msgTypes[34] mi := &file_api_v1_user_service_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -2429,7 +2761,7 @@ func (x *DeleteUserNotificationRequest) String() string { ...@@ -2429,7 +2761,7 @@ func (x *DeleteUserNotificationRequest) String() string {
func (*DeleteUserNotificationRequest) ProtoMessage() {} func (*DeleteUserNotificationRequest) ProtoMessage() {}
func (x *DeleteUserNotificationRequest) ProtoReflect() protoreflect.Message { func (x *DeleteUserNotificationRequest) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[34] mi := &file_api_v1_user_service_proto_msgTypes[40]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -2442,7 +2774,7 @@ func (x *DeleteUserNotificationRequest) ProtoReflect() protoreflect.Message { ...@@ -2442,7 +2774,7 @@ func (x *DeleteUserNotificationRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeleteUserNotificationRequest.ProtoReflect.Descriptor instead. // Deprecated: Use DeleteUserNotificationRequest.ProtoReflect.Descriptor instead.
func (*DeleteUserNotificationRequest) Descriptor() ([]byte, []int) { func (*DeleteUserNotificationRequest) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{34} return file_api_v1_user_service_proto_rawDescGZIP(), []int{40}
} }
func (x *DeleteUserNotificationRequest) GetName() string { func (x *DeleteUserNotificationRequest) GetName() string {
...@@ -2465,7 +2797,7 @@ type UserStats_MemoTypeStats struct { ...@@ -2465,7 +2797,7 @@ type UserStats_MemoTypeStats struct {
func (x *UserStats_MemoTypeStats) Reset() { func (x *UserStats_MemoTypeStats) Reset() {
*x = UserStats_MemoTypeStats{} *x = UserStats_MemoTypeStats{}
mi := &file_api_v1_user_service_proto_msgTypes[36] mi := &file_api_v1_user_service_proto_msgTypes[41]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -2477,7 +2809,7 @@ func (x *UserStats_MemoTypeStats) String() string { ...@@ -2477,7 +2809,7 @@ func (x *UserStats_MemoTypeStats) String() string {
func (*UserStats_MemoTypeStats) ProtoMessage() {} func (*UserStats_MemoTypeStats) ProtoMessage() {}
func (x *UserStats_MemoTypeStats) ProtoReflect() protoreflect.Message { func (x *UserStats_MemoTypeStats) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[36] mi := &file_api_v1_user_service_proto_msgTypes[41]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -2490,7 +2822,7 @@ func (x *UserStats_MemoTypeStats) ProtoReflect() protoreflect.Message { ...@@ -2490,7 +2822,7 @@ func (x *UserStats_MemoTypeStats) ProtoReflect() protoreflect.Message {
// Deprecated: Use UserStats_MemoTypeStats.ProtoReflect.Descriptor instead. // Deprecated: Use UserStats_MemoTypeStats.ProtoReflect.Descriptor instead.
func (*UserStats_MemoTypeStats) Descriptor() ([]byte, []int) { func (*UserStats_MemoTypeStats) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{9, 1} return file_api_v1_user_service_proto_rawDescGZIP(), []int{9, 0}
} }
func (x *UserStats_MemoTypeStats) GetLinkCount() int32 { func (x *UserStats_MemoTypeStats) GetLinkCount() int32 {
...@@ -2538,7 +2870,7 @@ type UserSetting_GeneralSetting struct { ...@@ -2538,7 +2870,7 @@ type UserSetting_GeneralSetting struct {
func (x *UserSetting_GeneralSetting) Reset() { func (x *UserSetting_GeneralSetting) Reset() {
*x = UserSetting_GeneralSetting{} *x = UserSetting_GeneralSetting{}
mi := &file_api_v1_user_service_proto_msgTypes[37] mi := &file_api_v1_user_service_proto_msgTypes[43]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -2550,7 +2882,7 @@ func (x *UserSetting_GeneralSetting) String() string { ...@@ -2550,7 +2882,7 @@ func (x *UserSetting_GeneralSetting) String() string {
func (*UserSetting_GeneralSetting) ProtoMessage() {} func (*UserSetting_GeneralSetting) ProtoMessage() {}
func (x *UserSetting_GeneralSetting) ProtoReflect() protoreflect.Message { func (x *UserSetting_GeneralSetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[37] mi := &file_api_v1_user_service_proto_msgTypes[43]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -2598,7 +2930,7 @@ type UserSetting_WebhooksSetting struct { ...@@ -2598,7 +2930,7 @@ type UserSetting_WebhooksSetting struct {
func (x *UserSetting_WebhooksSetting) Reset() { func (x *UserSetting_WebhooksSetting) Reset() {
*x = UserSetting_WebhooksSetting{} *x = UserSetting_WebhooksSetting{}
mi := &file_api_v1_user_service_proto_msgTypes[38] mi := &file_api_v1_user_service_proto_msgTypes[44]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -2610,7 +2942,7 @@ func (x *UserSetting_WebhooksSetting) String() string { ...@@ -2610,7 +2942,7 @@ func (x *UserSetting_WebhooksSetting) String() string {
func (*UserSetting_WebhooksSetting) ProtoMessage() {} func (*UserSetting_WebhooksSetting) ProtoMessage() {}
func (x *UserSetting_WebhooksSetting) ProtoReflect() protoreflect.Message { func (x *UserSetting_WebhooksSetting) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[38] mi := &file_api_v1_user_service_proto_msgTypes[44]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -2651,7 +2983,7 @@ type UserNotification_MemoCommentPayload struct { ...@@ -2651,7 +2983,7 @@ type UserNotification_MemoCommentPayload struct {
func (x *UserNotification_MemoCommentPayload) Reset() { func (x *UserNotification_MemoCommentPayload) Reset() {
*x = UserNotification_MemoCommentPayload{} *x = UserNotification_MemoCommentPayload{}
mi := &file_api_v1_user_service_proto_msgTypes[39] mi := &file_api_v1_user_service_proto_msgTypes[45]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -2663,7 +2995,7 @@ func (x *UserNotification_MemoCommentPayload) String() string { ...@@ -2663,7 +2995,7 @@ func (x *UserNotification_MemoCommentPayload) String() string {
func (*UserNotification_MemoCommentPayload) ProtoMessage() {} func (*UserNotification_MemoCommentPayload) ProtoMessage() {}
func (x *UserNotification_MemoCommentPayload) ProtoReflect() protoreflect.Message { func (x *UserNotification_MemoCommentPayload) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[39] mi := &file_api_v1_user_service_proto_msgTypes[45]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -2676,7 +3008,7 @@ func (x *UserNotification_MemoCommentPayload) ProtoReflect() protoreflect.Messag ...@@ -2676,7 +3008,7 @@ func (x *UserNotification_MemoCommentPayload) ProtoReflect() protoreflect.Messag
// Deprecated: Use UserNotification_MemoCommentPayload.ProtoReflect.Descriptor instead. // Deprecated: Use UserNotification_MemoCommentPayload.ProtoReflect.Descriptor instead.
func (*UserNotification_MemoCommentPayload) Descriptor() ([]byte, []int) { func (*UserNotification_MemoCommentPayload) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30, 0} return file_api_v1_user_service_proto_rawDescGZIP(), []int{36, 0}
} }
func (x *UserNotification_MemoCommentPayload) GetMemo() string { func (x *UserNotification_MemoCommentPayload) GetMemo() string {
...@@ -2725,7 +3057,7 @@ type UserNotification_MemoMentionPayload struct { ...@@ -2725,7 +3057,7 @@ type UserNotification_MemoMentionPayload struct {
func (x *UserNotification_MemoMentionPayload) Reset() { func (x *UserNotification_MemoMentionPayload) Reset() {
*x = UserNotification_MemoMentionPayload{} *x = UserNotification_MemoMentionPayload{}
mi := &file_api_v1_user_service_proto_msgTypes[40] mi := &file_api_v1_user_service_proto_msgTypes[46]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
...@@ -2737,7 +3069,7 @@ func (x *UserNotification_MemoMentionPayload) String() string { ...@@ -2737,7 +3069,7 @@ func (x *UserNotification_MemoMentionPayload) String() string {
func (*UserNotification_MemoMentionPayload) ProtoMessage() {} func (*UserNotification_MemoMentionPayload) ProtoMessage() {}
func (x *UserNotification_MemoMentionPayload) ProtoReflect() protoreflect.Message { func (x *UserNotification_MemoMentionPayload) ProtoReflect() protoreflect.Message {
mi := &file_api_v1_user_service_proto_msgTypes[40] mi := &file_api_v1_user_service_proto_msgTypes[46]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
...@@ -2750,7 +3082,7 @@ func (x *UserNotification_MemoMentionPayload) ProtoReflect() protoreflect.Messag ...@@ -2750,7 +3082,7 @@ func (x *UserNotification_MemoMentionPayload) ProtoReflect() protoreflect.Messag
// Deprecated: Use UserNotification_MemoMentionPayload.ProtoReflect.Descriptor instead. // Deprecated: Use UserNotification_MemoMentionPayload.ProtoReflect.Descriptor instead.
func (*UserNotification_MemoMentionPayload) Descriptor() ([]byte, []int) { func (*UserNotification_MemoMentionPayload) Descriptor() ([]byte, []int) {
return file_api_v1_user_service_proto_rawDescGZIP(), []int{30, 1} return file_api_v1_user_service_proto_rawDescGZIP(), []int{36, 1}
} }
func (x *UserNotification_MemoMentionPayload) GetMemo() string { func (x *UserNotification_MemoMentionPayload) GetMemo() string {
...@@ -2847,10 +3179,7 @@ const file_api_v1_user_service_proto_rawDesc = "" + ...@@ -2847,10 +3179,7 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"\x0fmemo_type_stats\x18\x03 \x01(\v2%.memos.api.v1.UserStats.MemoTypeStatsR\rmemoTypeStats\x12B\n" + "\x0fmemo_type_stats\x18\x03 \x01(\v2%.memos.api.v1.UserStats.MemoTypeStatsR\rmemoTypeStats\x12B\n" +
"\ttag_count\x18\x04 \x03(\v2%.memos.api.v1.UserStats.TagCountEntryR\btagCount\x12!\n" + "\ttag_count\x18\x04 \x03(\v2%.memos.api.v1.UserStats.TagCountEntryR\btagCount\x12!\n" +
"\fpinned_memos\x18\x05 \x03(\tR\vpinnedMemos\x12(\n" + "\fpinned_memos\x18\x05 \x03(\tR\vpinnedMemos\x12(\n" +
"\x10total_memo_count\x18\x06 \x01(\x05R\x0etotalMemoCount\x1a;\n" + "\x10total_memo_count\x18\x06 \x01(\x05R\x0etotalMemoCount\x1a\x8b\x01\n" +
"\rTagCountEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\x05R\x05value:\x028\x01\x1a\x8b\x01\n" +
"\rMemoTypeStats\x12\x1d\n" + "\rMemoTypeStats\x12\x1d\n" +
"\n" + "\n" +
"link_count\x18\x01 \x01(\x05R\tlinkCount\x12\x1d\n" + "link_count\x18\x01 \x01(\x05R\tlinkCount\x12\x1d\n" +
...@@ -2859,7 +3188,10 @@ const file_api_v1_user_service_proto_rawDesc = "" + ...@@ -2859,7 +3188,10 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"\n" + "\n" +
"todo_count\x18\x03 \x01(\x05R\ttodoCount\x12\x1d\n" + "todo_count\x18\x03 \x01(\x05R\ttodoCount\x12\x1d\n" +
"\n" + "\n" +
"undo_count\x18\x04 \x01(\x05R\tundoCount:?\xeaA<\n" + "undo_count\x18\x04 \x01(\x05R\tundoCount\x1a;\n" +
"\rTagCountEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\x05R\x05value:\x028\x01:?\xeaA<\n" +
"\x16memos.api.v1/UserStats\x12\fusers/{user}*\tuserStats2\tuserStats\"D\n" + "\x16memos.api.v1/UserStats\x12\fusers/{user}*\tuserStats2\tuserStats\"D\n" +
"\x13GetUserStatsRequest\x12-\n" + "\x13GetUserStatsRequest\x12-\n" +
"\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
...@@ -2900,7 +3232,33 @@ const file_api_v1_user_service_proto_rawDesc = "" + ...@@ -2900,7 +3232,33 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"\bsettings\x18\x01 \x03(\v2\x19.memos.api.v1.UserSettingR\bsettings\x12&\n" + "\bsettings\x18\x01 \x03(\v2\x19.memos.api.v1.UserSettingR\bsettings\x12&\n" +
"\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" +
"\n" + "\n" +
"total_size\x18\x03 \x01(\x05R\ttotalSize\"\xa7\x03\n" + "total_size\x18\x03 \x01(\x05R\ttotalSize\"\x84\x02\n" +
"\x0eLinkedIdentity\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12@\n" +
"\bidp_name\x18\x02 \x01(\tB%\xe0A\x03\xfaA\x1f\n" +
"\x1dmemos.api.v1/IdentityProviderR\aidpName\x12\"\n" +
"\n" +
"extern_uid\x18\x03 \x01(\tB\x03\xe0A\x03R\texternUid:s\xeaAp\n" +
"\x1bmemos.api.v1/LinkedIdentity\x12/users/{user}/linkedIdentities/{linked_identity}*\x10linkedIdentities2\x0elinkedIdentity\"P\n" +
"\x1bListLinkedIdentitiesRequest\x121\n" +
"\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
"\x11memos.api.v1/UserR\x06parent\"i\n" +
"\x1cListLinkedIdentitiesResponse\x12I\n" +
"\x11linked_identities\x18\x01 \x03(\v2\x1c.memos.api.v1.LinkedIdentityR\x10linkedIdentities\"\xfd\x01\n" +
"\x1bCreateLinkedIdentityRequest\x121\n" +
"\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
"\x11memos.api.v1/UserR\x06parent\x12@\n" +
"\bidp_name\x18\x02 \x01(\tB%\xe0A\x02\xfaA\x1f\n" +
"\x1dmemos.api.v1/IdentityProviderR\aidpName\x12\x17\n" +
"\x04code\x18\x03 \x01(\tB\x03\xe0A\x02R\x04code\x12&\n" +
"\fredirect_uri\x18\x04 \x01(\tB\x03\xe0A\x02R\vredirectUri\x12(\n" +
"\rcode_verifier\x18\x05 \x01(\tB\x03\xe0A\x01R\fcodeVerifier\"S\n" +
"\x18GetLinkedIdentityRequest\x127\n" +
"\x04name\x18\x01 \x01(\tB#\xe0A\x02\xfaA\x1d\n" +
"\x1bmemos.api.v1/LinkedIdentityR\x04name\"V\n" +
"\x1bDeleteLinkedIdentityRequest\x127\n" +
"\x04name\x18\x01 \x01(\tB#\xe0A\x02\xfaA\x1d\n" +
"\x1bmemos.api.v1/LinkedIdentityR\x04name\"\xa7\x03\n" +
"\x13PersonalAccessToken\x12\x17\n" + "\x13PersonalAccessToken\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12%\n" + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12%\n" +
"\vdescription\x18\x02 \x01(\tB\x03\xe0A\x01R\vdescription\x12>\n" + "\vdescription\x18\x02 \x01(\tB\x03\xe0A\x01R\vdescription\x12>\n" +
...@@ -3003,7 +3361,7 @@ const file_api_v1_user_service_proto_rawDesc = "" + ...@@ -3003,7 +3361,7 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"updateMask\"Z\n" + "updateMask\"Z\n" +
"\x1dDeleteUserNotificationRequest\x129\n" + "\x1dDeleteUserNotificationRequest\x129\n" +
"\x04name\x18\x01 \x01(\tB%\xe0A\x02\xfaA\x1f\n" + "\x04name\x18\x01 \x01(\tB%\xe0A\x02\xfaA\x1f\n" +
"\x1dmemos.api.v1/UserNotificationR\x04name2\x80\x18\n" + "\x1dmemos.api.v1/UserNotificationR\x04name2\x82\x1d\n" +
"\vUserService\x12c\n" + "\vUserService\x12c\n" +
"\tListUsers\x12\x1e.memos.api.v1.ListUsersRequest\x1a\x1f.memos.api.v1.ListUsersResponse\"\x15\x82\xd3\xe4\x93\x02\x0f\x12\r/api/v1/users\x12{\n" + "\tListUsers\x12\x1e.memos.api.v1.ListUsersRequest\x1a\x1f.memos.api.v1.ListUsersResponse\"\x15\x82\xd3\xe4\x93\x02\x0f\x12\r/api/v1/users\x12{\n" +
"\rBatchGetUsers\x12\".memos.api.v1.BatchGetUsersRequest\x1a#.memos.api.v1.BatchGetUsersResponse\"!\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/api/v1/users:batchGet\x12b\n" + "\rBatchGetUsers\x12\".memos.api.v1.BatchGetUsersRequest\x1a#.memos.api.v1.BatchGetUsersResponse\"!\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/api/v1/users:batchGet\x12b\n" +
...@@ -3018,7 +3376,11 @@ const file_api_v1_user_service_proto_rawDesc = "" + ...@@ -3018,7 +3376,11 @@ const file_api_v1_user_service_proto_rawDesc = "" +
"\fGetUserStats\x12!.memos.api.v1.GetUserStatsRequest\x1a\x17.memos.api.v1.UserStats\".\xdaA\x04name\x82\xd3\xe4\x93\x02!\x12\x1f/api/v1/{name=users/*}:getStats\x12\x82\x01\n" + "\fGetUserStats\x12!.memos.api.v1.GetUserStatsRequest\x1a\x17.memos.api.v1.UserStats\".\xdaA\x04name\x82\xd3\xe4\x93\x02!\x12\x1f/api/v1/{name=users/*}:getStats\x12\x82\x01\n" +
"\x0eGetUserSetting\x12#.memos.api.v1.GetUserSettingRequest\x1a\x19.memos.api.v1.UserSetting\"0\xdaA\x04name\x82\xd3\xe4\x93\x02#\x12!/api/v1/{name=users/*/settings/*}\x12\xa8\x01\n" + "\x0eGetUserSetting\x12#.memos.api.v1.GetUserSettingRequest\x1a\x19.memos.api.v1.UserSetting\"0\xdaA\x04name\x82\xd3\xe4\x93\x02#\x12!/api/v1/{name=users/*/settings/*}\x12\xa8\x01\n" +
"\x11UpdateUserSetting\x12&.memos.api.v1.UpdateUserSettingRequest\x1a\x19.memos.api.v1.UserSetting\"P\xdaA\x13setting,update_mask\x82\xd3\xe4\x93\x024:\asetting2)/api/v1/{setting.name=users/*/settings/*}\x12\x95\x01\n" + "\x11UpdateUserSetting\x12&.memos.api.v1.UpdateUserSettingRequest\x1a\x19.memos.api.v1.UserSetting\"P\xdaA\x13setting,update_mask\x82\xd3\xe4\x93\x024:\asetting2)/api/v1/{setting.name=users/*/settings/*}\x12\x95\x01\n" +
"\x10ListUserSettings\x12%.memos.api.v1.ListUserSettingsRequest\x1a&.memos.api.v1.ListUserSettingsResponse\"2\xdaA\x06parent\x82\xd3\xe4\x93\x02#\x12!/api/v1/{parent=users/*}/settings\x12\xb9\x01\n" + "\x10ListUserSettings\x12%.memos.api.v1.ListUserSettingsRequest\x1a&.memos.api.v1.ListUserSettingsResponse\"2\xdaA\x06parent\x82\xd3\xe4\x93\x02#\x12!/api/v1/{parent=users/*}/settings\x12\xa9\x01\n" +
"\x14ListLinkedIdentities\x12).memos.api.v1.ListLinkedIdentitiesRequest\x1a*.memos.api.v1.ListLinkedIdentitiesResponse\":\xdaA\x06parent\x82\xd3\xe4\x93\x02+\x12)/api/v1/{parent=users/*}/linkedIdentities\x12\xa7\x01\n" +
"\x14CreateLinkedIdentity\x12).memos.api.v1.CreateLinkedIdentityRequest\x1a\x1c.memos.api.v1.LinkedIdentity\"F\xdaA\x0fparent,idp_name\x82\xd3\xe4\x93\x02.:\x01*\")/api/v1/{parent=users/*}/linkedIdentities\x12\x93\x01\n" +
"\x11GetLinkedIdentity\x12&.memos.api.v1.GetLinkedIdentityRequest\x1a\x1c.memos.api.v1.LinkedIdentity\"8\xdaA\x04name\x82\xd3\xe4\x93\x02+\x12)/api/v1/{name=users/*/linkedIdentities/*}\x12\x93\x01\n" +
"\x14DeleteLinkedIdentity\x12).memos.api.v1.DeleteLinkedIdentityRequest\x1a\x16.google.protobuf.Empty\"8\xdaA\x04name\x82\xd3\xe4\x93\x02+*)/api/v1/{name=users/*/linkedIdentities/*}\x12\xb9\x01\n" +
"\x18ListPersonalAccessTokens\x12-.memos.api.v1.ListPersonalAccessTokensRequest\x1a..memos.api.v1.ListPersonalAccessTokensResponse\">\xdaA\x06parent\x82\xd3\xe4\x93\x02/\x12-/api/v1/{parent=users/*}/personalAccessTokens\x12\xb6\x01\n" + "\x18ListPersonalAccessTokens\x12-.memos.api.v1.ListPersonalAccessTokensRequest\x1a..memos.api.v1.ListPersonalAccessTokensResponse\">\xdaA\x06parent\x82\xd3\xe4\x93\x02/\x12-/api/v1/{parent=users/*}/personalAccessTokens\x12\xb6\x01\n" +
"\x19CreatePersonalAccessToken\x12..memos.api.v1.CreatePersonalAccessTokenRequest\x1a/.memos.api.v1.CreatePersonalAccessTokenResponse\"8\x82\xd3\xe4\x93\x022:\x01*\"-/api/v1/{parent=users/*}/personalAccessTokens\x12\xa1\x01\n" + "\x19CreatePersonalAccessToken\x12..memos.api.v1.CreatePersonalAccessTokenRequest\x1a/.memos.api.v1.CreatePersonalAccessTokenResponse\"8\x82\xd3\xe4\x93\x022:\x01*\"-/api/v1/{parent=users/*}/personalAccessTokens\x12\xa1\x01\n" +
"\x19DeletePersonalAccessToken\x12..memos.api.v1.DeletePersonalAccessTokenRequest\x1a\x16.google.protobuf.Empty\"<\xdaA\x04name\x82\xd3\xe4\x93\x02/*-/api/v1/{name=users/*/personalAccessTokens/*}\x12\x95\x01\n" + "\x19DeletePersonalAccessToken\x12..memos.api.v1.DeletePersonalAccessTokenRequest\x1a\x16.google.protobuf.Empty\"<\xdaA\x04name\x82\xd3\xe4\x93\x02/*-/api/v1/{name=users/*/personalAccessTokens/*}\x12\x95\x01\n" +
...@@ -3044,7 +3406,7 @@ func file_api_v1_user_service_proto_rawDescGZIP() []byte { ...@@ -3044,7 +3406,7 @@ func file_api_v1_user_service_proto_rawDescGZIP() []byte {
} }
var file_api_v1_user_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4) var file_api_v1_user_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 41) var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 47)
var file_api_v1_user_service_proto_goTypes = []any{ var file_api_v1_user_service_proto_goTypes = []any{
(User_Role)(0), // 0: memos.api.v1.User.Role (User_Role)(0), // 0: memos.api.v1.User.Role
(UserSetting_Key)(0), // 1: memos.api.v1.UserSetting.Key (UserSetting_Key)(0), // 1: memos.api.v1.UserSetting.Key
...@@ -3068,122 +3430,137 @@ var file_api_v1_user_service_proto_goTypes = []any{ ...@@ -3068,122 +3430,137 @@ var file_api_v1_user_service_proto_goTypes = []any{
(*UpdateUserSettingRequest)(nil), // 19: memos.api.v1.UpdateUserSettingRequest (*UpdateUserSettingRequest)(nil), // 19: memos.api.v1.UpdateUserSettingRequest
(*ListUserSettingsRequest)(nil), // 20: memos.api.v1.ListUserSettingsRequest (*ListUserSettingsRequest)(nil), // 20: memos.api.v1.ListUserSettingsRequest
(*ListUserSettingsResponse)(nil), // 21: memos.api.v1.ListUserSettingsResponse (*ListUserSettingsResponse)(nil), // 21: memos.api.v1.ListUserSettingsResponse
(*PersonalAccessToken)(nil), // 22: memos.api.v1.PersonalAccessToken (*LinkedIdentity)(nil), // 22: memos.api.v1.LinkedIdentity
(*ListPersonalAccessTokensRequest)(nil), // 23: memos.api.v1.ListPersonalAccessTokensRequest (*ListLinkedIdentitiesRequest)(nil), // 23: memos.api.v1.ListLinkedIdentitiesRequest
(*ListPersonalAccessTokensResponse)(nil), // 24: memos.api.v1.ListPersonalAccessTokensResponse (*ListLinkedIdentitiesResponse)(nil), // 24: memos.api.v1.ListLinkedIdentitiesResponse
(*CreatePersonalAccessTokenRequest)(nil), // 25: memos.api.v1.CreatePersonalAccessTokenRequest (*CreateLinkedIdentityRequest)(nil), // 25: memos.api.v1.CreateLinkedIdentityRequest
(*CreatePersonalAccessTokenResponse)(nil), // 26: memos.api.v1.CreatePersonalAccessTokenResponse (*GetLinkedIdentityRequest)(nil), // 26: memos.api.v1.GetLinkedIdentityRequest
(*DeletePersonalAccessTokenRequest)(nil), // 27: memos.api.v1.DeletePersonalAccessTokenRequest (*DeleteLinkedIdentityRequest)(nil), // 27: memos.api.v1.DeleteLinkedIdentityRequest
(*UserWebhook)(nil), // 28: memos.api.v1.UserWebhook (*PersonalAccessToken)(nil), // 28: memos.api.v1.PersonalAccessToken
(*ListUserWebhooksRequest)(nil), // 29: memos.api.v1.ListUserWebhooksRequest (*ListPersonalAccessTokensRequest)(nil), // 29: memos.api.v1.ListPersonalAccessTokensRequest
(*ListUserWebhooksResponse)(nil), // 30: memos.api.v1.ListUserWebhooksResponse (*ListPersonalAccessTokensResponse)(nil), // 30: memos.api.v1.ListPersonalAccessTokensResponse
(*CreateUserWebhookRequest)(nil), // 31: memos.api.v1.CreateUserWebhookRequest (*CreatePersonalAccessTokenRequest)(nil), // 31: memos.api.v1.CreatePersonalAccessTokenRequest
(*UpdateUserWebhookRequest)(nil), // 32: memos.api.v1.UpdateUserWebhookRequest (*CreatePersonalAccessTokenResponse)(nil), // 32: memos.api.v1.CreatePersonalAccessTokenResponse
(*DeleteUserWebhookRequest)(nil), // 33: memos.api.v1.DeleteUserWebhookRequest (*DeletePersonalAccessTokenRequest)(nil), // 33: memos.api.v1.DeletePersonalAccessTokenRequest
(*UserNotification)(nil), // 34: memos.api.v1.UserNotification (*UserWebhook)(nil), // 34: memos.api.v1.UserWebhook
(*ListUserNotificationsRequest)(nil), // 35: memos.api.v1.ListUserNotificationsRequest (*ListUserWebhooksRequest)(nil), // 35: memos.api.v1.ListUserWebhooksRequest
(*ListUserNotificationsResponse)(nil), // 36: memos.api.v1.ListUserNotificationsResponse (*ListUserWebhooksResponse)(nil), // 36: memos.api.v1.ListUserWebhooksResponse
(*UpdateUserNotificationRequest)(nil), // 37: memos.api.v1.UpdateUserNotificationRequest (*CreateUserWebhookRequest)(nil), // 37: memos.api.v1.CreateUserWebhookRequest
(*DeleteUserNotificationRequest)(nil), // 38: memos.api.v1.DeleteUserNotificationRequest (*UpdateUserWebhookRequest)(nil), // 38: memos.api.v1.UpdateUserWebhookRequest
nil, // 39: memos.api.v1.UserStats.TagCountEntry (*DeleteUserWebhookRequest)(nil), // 39: memos.api.v1.DeleteUserWebhookRequest
(*UserStats_MemoTypeStats)(nil), // 40: memos.api.v1.UserStats.MemoTypeStats (*UserNotification)(nil), // 40: memos.api.v1.UserNotification
(*UserSetting_GeneralSetting)(nil), // 41: memos.api.v1.UserSetting.GeneralSetting (*ListUserNotificationsRequest)(nil), // 41: memos.api.v1.ListUserNotificationsRequest
(*UserSetting_WebhooksSetting)(nil), // 42: memos.api.v1.UserSetting.WebhooksSetting (*ListUserNotificationsResponse)(nil), // 42: memos.api.v1.ListUserNotificationsResponse
(*UserNotification_MemoCommentPayload)(nil), // 43: memos.api.v1.UserNotification.MemoCommentPayload (*UpdateUserNotificationRequest)(nil), // 43: memos.api.v1.UpdateUserNotificationRequest
(*UserNotification_MemoMentionPayload)(nil), // 44: memos.api.v1.UserNotification.MemoMentionPayload (*DeleteUserNotificationRequest)(nil), // 44: memos.api.v1.DeleteUserNotificationRequest
(State)(0), // 45: memos.api.v1.State (*UserStats_MemoTypeStats)(nil), // 45: memos.api.v1.UserStats.MemoTypeStats
(*timestamppb.Timestamp)(nil), // 46: google.protobuf.Timestamp nil, // 46: memos.api.v1.UserStats.TagCountEntry
(*fieldmaskpb.FieldMask)(nil), // 47: google.protobuf.FieldMask (*UserSetting_GeneralSetting)(nil), // 47: memos.api.v1.UserSetting.GeneralSetting
(*emptypb.Empty)(nil), // 48: google.protobuf.Empty (*UserSetting_WebhooksSetting)(nil), // 48: memos.api.v1.UserSetting.WebhooksSetting
(*UserNotification_MemoCommentPayload)(nil), // 49: memos.api.v1.UserNotification.MemoCommentPayload
(*UserNotification_MemoMentionPayload)(nil), // 50: memos.api.v1.UserNotification.MemoMentionPayload
(State)(0), // 51: memos.api.v1.State
(*timestamppb.Timestamp)(nil), // 52: google.protobuf.Timestamp
(*fieldmaskpb.FieldMask)(nil), // 53: google.protobuf.FieldMask
(*emptypb.Empty)(nil), // 54: google.protobuf.Empty
} }
var file_api_v1_user_service_proto_depIdxs = []int32{ var file_api_v1_user_service_proto_depIdxs = []int32{
0, // 0: memos.api.v1.User.role:type_name -> memos.api.v1.User.Role 0, // 0: memos.api.v1.User.role:type_name -> memos.api.v1.User.Role
45, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State 51, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State
46, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp 52, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp
46, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp 52, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp
4, // 4: memos.api.v1.ListUsersResponse.users:type_name -> memos.api.v1.User 4, // 4: memos.api.v1.ListUsersResponse.users:type_name -> memos.api.v1.User
4, // 5: memos.api.v1.BatchGetUsersResponse.users:type_name -> memos.api.v1.User 4, // 5: memos.api.v1.BatchGetUsersResponse.users:type_name -> memos.api.v1.User
47, // 6: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask 53, // 6: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask
4, // 7: memos.api.v1.CreateUserRequest.user:type_name -> memos.api.v1.User 4, // 7: memos.api.v1.CreateUserRequest.user:type_name -> memos.api.v1.User
4, // 8: memos.api.v1.UpdateUserRequest.user:type_name -> memos.api.v1.User 4, // 8: memos.api.v1.UpdateUserRequest.user:type_name -> memos.api.v1.User
47, // 9: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask 53, // 9: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask
46, // 10: memos.api.v1.UserStats.memo_display_timestamps:type_name -> google.protobuf.Timestamp 52, // 10: memos.api.v1.UserStats.memo_display_timestamps:type_name -> google.protobuf.Timestamp
40, // 11: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats 45, // 11: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats
39, // 12: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry 46, // 12: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry
13, // 13: memos.api.v1.ListAllUserStatsResponse.stats:type_name -> memos.api.v1.UserStats 13, // 13: memos.api.v1.ListAllUserStatsResponse.stats:type_name -> memos.api.v1.UserStats
41, // 14: memos.api.v1.UserSetting.general_setting:type_name -> memos.api.v1.UserSetting.GeneralSetting 47, // 14: memos.api.v1.UserSetting.general_setting:type_name -> memos.api.v1.UserSetting.GeneralSetting
42, // 15: memos.api.v1.UserSetting.webhooks_setting:type_name -> memos.api.v1.UserSetting.WebhooksSetting 48, // 15: memos.api.v1.UserSetting.webhooks_setting:type_name -> memos.api.v1.UserSetting.WebhooksSetting
17, // 16: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting 17, // 16: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting
47, // 17: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask 53, // 17: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask
17, // 18: memos.api.v1.ListUserSettingsResponse.settings:type_name -> memos.api.v1.UserSetting 17, // 18: memos.api.v1.ListUserSettingsResponse.settings:type_name -> memos.api.v1.UserSetting
46, // 19: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp 22, // 19: memos.api.v1.ListLinkedIdentitiesResponse.linked_identities:type_name -> memos.api.v1.LinkedIdentity
46, // 20: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp 52, // 20: memos.api.v1.PersonalAccessToken.created_at:type_name -> google.protobuf.Timestamp
46, // 21: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp 52, // 21: memos.api.v1.PersonalAccessToken.expires_at:type_name -> google.protobuf.Timestamp
22, // 22: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken 52, // 22: memos.api.v1.PersonalAccessToken.last_used_at:type_name -> google.protobuf.Timestamp
22, // 23: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken 28, // 23: memos.api.v1.ListPersonalAccessTokensResponse.personal_access_tokens:type_name -> memos.api.v1.PersonalAccessToken
46, // 24: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp 28, // 24: memos.api.v1.CreatePersonalAccessTokenResponse.personal_access_token:type_name -> memos.api.v1.PersonalAccessToken
46, // 25: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp 52, // 25: memos.api.v1.UserWebhook.create_time:type_name -> google.protobuf.Timestamp
28, // 26: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook 52, // 26: memos.api.v1.UserWebhook.update_time:type_name -> google.protobuf.Timestamp
28, // 27: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook 34, // 27: memos.api.v1.ListUserWebhooksResponse.webhooks:type_name -> memos.api.v1.UserWebhook
28, // 28: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook 34, // 28: memos.api.v1.CreateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
47, // 29: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask 34, // 29: memos.api.v1.UpdateUserWebhookRequest.webhook:type_name -> memos.api.v1.UserWebhook
4, // 30: memos.api.v1.UserNotification.sender_user:type_name -> memos.api.v1.User 53, // 30: memos.api.v1.UpdateUserWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask
2, // 31: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status 4, // 31: memos.api.v1.UserNotification.sender_user:type_name -> memos.api.v1.User
46, // 32: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp 2, // 32: memos.api.v1.UserNotification.status:type_name -> memos.api.v1.UserNotification.Status
3, // 33: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type 52, // 33: memos.api.v1.UserNotification.create_time:type_name -> google.protobuf.Timestamp
43, // 34: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload 3, // 34: memos.api.v1.UserNotification.type:type_name -> memos.api.v1.UserNotification.Type
44, // 35: memos.api.v1.UserNotification.memo_mention:type_name -> memos.api.v1.UserNotification.MemoMentionPayload 49, // 35: memos.api.v1.UserNotification.memo_comment:type_name -> memos.api.v1.UserNotification.MemoCommentPayload
34, // 36: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification 50, // 36: memos.api.v1.UserNotification.memo_mention:type_name -> memos.api.v1.UserNotification.MemoMentionPayload
34, // 37: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification 40, // 37: memos.api.v1.ListUserNotificationsResponse.notifications:type_name -> memos.api.v1.UserNotification
47, // 38: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask 40, // 38: memos.api.v1.UpdateUserNotificationRequest.notification:type_name -> memos.api.v1.UserNotification
28, // 39: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook 53, // 39: memos.api.v1.UpdateUserNotificationRequest.update_mask:type_name -> google.protobuf.FieldMask
5, // 40: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest 34, // 40: memos.api.v1.UserSetting.WebhooksSetting.webhooks:type_name -> memos.api.v1.UserWebhook
7, // 41: memos.api.v1.UserService.BatchGetUsers:input_type -> memos.api.v1.BatchGetUsersRequest 5, // 41: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest
9, // 42: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest 7, // 42: memos.api.v1.UserService.BatchGetUsers:input_type -> memos.api.v1.BatchGetUsersRequest
10, // 43: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest 9, // 43: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest
11, // 44: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest 10, // 44: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest
12, // 45: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest 11, // 45: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest
15, // 46: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest 12, // 46: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest
14, // 47: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest 15, // 47: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest
18, // 48: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest 14, // 48: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest
19, // 49: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest 18, // 49: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest
20, // 50: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest 19, // 50: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest
23, // 51: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest 20, // 51: memos.api.v1.UserService.ListUserSettings:input_type -> memos.api.v1.ListUserSettingsRequest
25, // 52: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest 23, // 52: memos.api.v1.UserService.ListLinkedIdentities:input_type -> memos.api.v1.ListLinkedIdentitiesRequest
27, // 53: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest 25, // 53: memos.api.v1.UserService.CreateLinkedIdentity:input_type -> memos.api.v1.CreateLinkedIdentityRequest
29, // 54: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest 26, // 54: memos.api.v1.UserService.GetLinkedIdentity:input_type -> memos.api.v1.GetLinkedIdentityRequest
31, // 55: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest 27, // 55: memos.api.v1.UserService.DeleteLinkedIdentity:input_type -> memos.api.v1.DeleteLinkedIdentityRequest
32, // 56: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest 29, // 56: memos.api.v1.UserService.ListPersonalAccessTokens:input_type -> memos.api.v1.ListPersonalAccessTokensRequest
33, // 57: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest 31, // 57: memos.api.v1.UserService.CreatePersonalAccessToken:input_type -> memos.api.v1.CreatePersonalAccessTokenRequest
35, // 58: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest 33, // 58: memos.api.v1.UserService.DeletePersonalAccessToken:input_type -> memos.api.v1.DeletePersonalAccessTokenRequest
37, // 59: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest 35, // 59: memos.api.v1.UserService.ListUserWebhooks:input_type -> memos.api.v1.ListUserWebhooksRequest
38, // 60: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest 37, // 60: memos.api.v1.UserService.CreateUserWebhook:input_type -> memos.api.v1.CreateUserWebhookRequest
6, // 61: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse 38, // 61: memos.api.v1.UserService.UpdateUserWebhook:input_type -> memos.api.v1.UpdateUserWebhookRequest
8, // 62: memos.api.v1.UserService.BatchGetUsers:output_type -> memos.api.v1.BatchGetUsersResponse 39, // 62: memos.api.v1.UserService.DeleteUserWebhook:input_type -> memos.api.v1.DeleteUserWebhookRequest
4, // 63: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User 41, // 63: memos.api.v1.UserService.ListUserNotifications:input_type -> memos.api.v1.ListUserNotificationsRequest
4, // 64: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User 43, // 64: memos.api.v1.UserService.UpdateUserNotification:input_type -> memos.api.v1.UpdateUserNotificationRequest
4, // 65: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User 44, // 65: memos.api.v1.UserService.DeleteUserNotification:input_type -> memos.api.v1.DeleteUserNotificationRequest
48, // 66: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty 6, // 66: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse
16, // 67: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse 8, // 67: memos.api.v1.UserService.BatchGetUsers:output_type -> memos.api.v1.BatchGetUsersResponse
13, // 68: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats 4, // 68: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User
17, // 69: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting 4, // 69: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User
17, // 70: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting 4, // 70: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User
21, // 71: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse 54, // 71: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty
24, // 72: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse 16, // 72: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse
26, // 73: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse 13, // 73: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats
48, // 74: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty 17, // 74: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting
30, // 75: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse 17, // 75: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting
28, // 76: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook 21, // 76: memos.api.v1.UserService.ListUserSettings:output_type -> memos.api.v1.ListUserSettingsResponse
28, // 77: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook 24, // 77: memos.api.v1.UserService.ListLinkedIdentities:output_type -> memos.api.v1.ListLinkedIdentitiesResponse
48, // 78: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty 22, // 78: memos.api.v1.UserService.CreateLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity
36, // 79: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse 22, // 79: memos.api.v1.UserService.GetLinkedIdentity:output_type -> memos.api.v1.LinkedIdentity
34, // 80: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification 54, // 80: memos.api.v1.UserService.DeleteLinkedIdentity:output_type -> google.protobuf.Empty
48, // 81: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty 30, // 81: memos.api.v1.UserService.ListPersonalAccessTokens:output_type -> memos.api.v1.ListPersonalAccessTokensResponse
61, // [61:82] is the sub-list for method output_type 32, // 82: memos.api.v1.UserService.CreatePersonalAccessToken:output_type -> memos.api.v1.CreatePersonalAccessTokenResponse
40, // [40:61] is the sub-list for method input_type 54, // 83: memos.api.v1.UserService.DeletePersonalAccessToken:output_type -> google.protobuf.Empty
40, // [40:40] is the sub-list for extension type_name 36, // 84: memos.api.v1.UserService.ListUserWebhooks:output_type -> memos.api.v1.ListUserWebhooksResponse
40, // [40:40] is the sub-list for extension extendee 34, // 85: memos.api.v1.UserService.CreateUserWebhook:output_type -> memos.api.v1.UserWebhook
0, // [0:40] is the sub-list for field type_name 34, // 86: memos.api.v1.UserService.UpdateUserWebhook:output_type -> memos.api.v1.UserWebhook
54, // 87: memos.api.v1.UserService.DeleteUserWebhook:output_type -> google.protobuf.Empty
42, // 88: memos.api.v1.UserService.ListUserNotifications:output_type -> memos.api.v1.ListUserNotificationsResponse
40, // 89: memos.api.v1.UserService.UpdateUserNotification:output_type -> memos.api.v1.UserNotification
54, // 90: memos.api.v1.UserService.DeleteUserNotification:output_type -> google.protobuf.Empty
66, // [66:91] is the sub-list for method output_type
41, // [41:66] is the sub-list for method input_type
41, // [41:41] is the sub-list for extension type_name
41, // [41:41] is the sub-list for extension extendee
0, // [0:41] is the sub-list for field type_name
} }
func init() { file_api_v1_user_service_proto_init() } func init() { file_api_v1_user_service_proto_init() }
...@@ -3196,7 +3573,7 @@ func file_api_v1_user_service_proto_init() { ...@@ -3196,7 +3573,7 @@ func file_api_v1_user_service_proto_init() {
(*UserSetting_GeneralSetting_)(nil), (*UserSetting_GeneralSetting_)(nil),
(*UserSetting_WebhooksSetting_)(nil), (*UserSetting_WebhooksSetting_)(nil),
} }
file_api_v1_user_service_proto_msgTypes[30].OneofWrappers = []any{ file_api_v1_user_service_proto_msgTypes[36].OneofWrappers = []any{
(*UserNotification_MemoComment)(nil), (*UserNotification_MemoComment)(nil),
(*UserNotification_MemoMention)(nil), (*UserNotification_MemoMention)(nil),
} }
...@@ -3206,7 +3583,7 @@ func file_api_v1_user_service_proto_init() { ...@@ -3206,7 +3583,7 @@ func file_api_v1_user_service_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_user_service_proto_rawDesc), len(file_api_v1_user_service_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_user_service_proto_rawDesc), len(file_api_v1_user_service_proto_rawDesc)),
NumEnums: 4, NumEnums: 4,
NumMessages: 41, NumMessages: 47,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },
......
...@@ -558,6 +558,168 @@ func local_request_UserService_ListUserSettings_0(ctx context.Context, marshaler ...@@ -558,6 +558,168 @@ func local_request_UserService_ListUserSettings_0(ctx context.Context, marshaler
return msg, metadata, err return msg, metadata, err
} }
func request_UserService_ListLinkedIdentities_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ListLinkedIdentitiesRequest
metadata runtime.ServerMetadata
err error
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
val, ok := pathParams["parent"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent")
}
protoReq.Parent, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
}
msg, err := client.ListLinkedIdentities(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_ListLinkedIdentities_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ListLinkedIdentitiesRequest
metadata runtime.ServerMetadata
err error
)
val, ok := pathParams["parent"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent")
}
protoReq.Parent, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
}
msg, err := server.ListLinkedIdentities(ctx, &protoReq)
return msg, metadata, err
}
func request_UserService_CreateLinkedIdentity_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq CreateLinkedIdentityRequest
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
val, ok := pathParams["parent"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent")
}
protoReq.Parent, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
}
msg, err := client.CreateLinkedIdentity(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_CreateLinkedIdentity_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq CreateLinkedIdentityRequest
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
val, ok := pathParams["parent"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent")
}
protoReq.Parent, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err)
}
msg, err := server.CreateLinkedIdentity(ctx, &protoReq)
return msg, metadata, err
}
func request_UserService_GetLinkedIdentity_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetLinkedIdentityRequest
metadata runtime.ServerMetadata
err error
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
val, ok := pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := client.GetLinkedIdentity(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_GetLinkedIdentity_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetLinkedIdentityRequest
metadata runtime.ServerMetadata
err error
)
val, ok := pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := server.GetLinkedIdentity(ctx, &protoReq)
return msg, metadata, err
}
func request_UserService_DeleteLinkedIdentity_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeleteLinkedIdentityRequest
metadata runtime.ServerMetadata
err error
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
val, ok := pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := client.DeleteLinkedIdentity(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_UserService_DeleteLinkedIdentity_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeleteLinkedIdentityRequest
metadata runtime.ServerMetadata
err error
)
val, ok := pathParams["name"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
}
protoReq.Name, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
}
msg, err := server.DeleteLinkedIdentity(ctx, &protoReq)
return msg, metadata, err
}
var filter_UserService_ListPersonalAccessTokens_0 = &utilities.DoubleArray{Encoding: map[string]int{"parent": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} var filter_UserService_ListPersonalAccessTokens_0 = &utilities.DoubleArray{Encoding: map[string]int{"parent": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
func request_UserService_ListPersonalAccessTokens_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { func request_UserService_ListPersonalAccessTokens_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
...@@ -1298,6 +1460,86 @@ func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux ...@@ -1298,6 +1460,86 @@ func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
} }
forward_UserService_ListUserSettings_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) forward_UserService_ListUserSettings_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
}) })
mux.Handle(http.MethodGet, pattern_UserService_ListLinkedIdentities_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/ListLinkedIdentities", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/linkedIdentities"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_ListLinkedIdentities_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_ListLinkedIdentities_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_UserService_CreateLinkedIdentity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/CreateLinkedIdentity", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/linkedIdentities"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_CreateLinkedIdentity_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_CreateLinkedIdentity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_GetLinkedIdentity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/GetLinkedIdentity", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/linkedIdentities/*}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_GetLinkedIdentity_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_GetLinkedIdentity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodDelete, pattern_UserService_DeleteLinkedIdentity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/DeleteLinkedIdentity", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/linkedIdentities/*}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_UserService_DeleteLinkedIdentity_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_DeleteLinkedIdentity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_ListPersonalAccessTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle(http.MethodGet, pattern_UserService_ListPersonalAccessTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
...@@ -1725,6 +1967,74 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux ...@@ -1725,6 +1967,74 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
} }
forward_UserService_ListUserSettings_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) forward_UserService_ListUserSettings_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
}) })
mux.Handle(http.MethodGet, pattern_UserService_ListLinkedIdentities_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/ListLinkedIdentities", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/linkedIdentities"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_ListLinkedIdentities_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_ListLinkedIdentities_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_UserService_CreateLinkedIdentity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/CreateLinkedIdentity", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/linkedIdentities"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_CreateLinkedIdentity_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_CreateLinkedIdentity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_GetLinkedIdentity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/GetLinkedIdentity", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/linkedIdentities/*}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_GetLinkedIdentity_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_GetLinkedIdentity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodDelete, pattern_UserService_DeleteLinkedIdentity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/DeleteLinkedIdentity", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/linkedIdentities/*}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_UserService_DeleteLinkedIdentity_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_UserService_DeleteLinkedIdentity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_UserService_ListPersonalAccessTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle(http.MethodGet, pattern_UserService_ListPersonalAccessTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
...@@ -1910,6 +2220,10 @@ var ( ...@@ -1910,6 +2220,10 @@ var (
pattern_UserService_GetUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "settings", "name"}, "")) pattern_UserService_GetUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "settings", "name"}, ""))
pattern_UserService_UpdateUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "settings", "setting.name"}, "")) pattern_UserService_UpdateUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "settings", "setting.name"}, ""))
pattern_UserService_ListUserSettings_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "settings"}, "")) pattern_UserService_ListUserSettings_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "settings"}, ""))
pattern_UserService_ListLinkedIdentities_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "linkedIdentities"}, ""))
pattern_UserService_CreateLinkedIdentity_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "linkedIdentities"}, ""))
pattern_UserService_GetLinkedIdentity_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "linkedIdentities", "name"}, ""))
pattern_UserService_DeleteLinkedIdentity_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "linkedIdentities", "name"}, ""))
pattern_UserService_ListPersonalAccessTokens_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "personalAccessTokens"}, "")) pattern_UserService_ListPersonalAccessTokens_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "personalAccessTokens"}, ""))
pattern_UserService_CreatePersonalAccessToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "personalAccessTokens"}, "")) pattern_UserService_CreatePersonalAccessToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "personalAccessTokens"}, ""))
pattern_UserService_DeletePersonalAccessToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "personalAccessTokens", "name"}, "")) pattern_UserService_DeletePersonalAccessToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "personalAccessTokens", "name"}, ""))
...@@ -1934,6 +2248,10 @@ var ( ...@@ -1934,6 +2248,10 @@ var (
forward_UserService_GetUserSetting_0 = runtime.ForwardResponseMessage forward_UserService_GetUserSetting_0 = runtime.ForwardResponseMessage
forward_UserService_UpdateUserSetting_0 = runtime.ForwardResponseMessage forward_UserService_UpdateUserSetting_0 = runtime.ForwardResponseMessage
forward_UserService_ListUserSettings_0 = runtime.ForwardResponseMessage forward_UserService_ListUserSettings_0 = runtime.ForwardResponseMessage
forward_UserService_ListLinkedIdentities_0 = runtime.ForwardResponseMessage
forward_UserService_CreateLinkedIdentity_0 = runtime.ForwardResponseMessage
forward_UserService_GetLinkedIdentity_0 = runtime.ForwardResponseMessage
forward_UserService_DeleteLinkedIdentity_0 = runtime.ForwardResponseMessage
forward_UserService_ListPersonalAccessTokens_0 = runtime.ForwardResponseMessage forward_UserService_ListPersonalAccessTokens_0 = runtime.ForwardResponseMessage
forward_UserService_CreatePersonalAccessToken_0 = runtime.ForwardResponseMessage forward_UserService_CreatePersonalAccessToken_0 = runtime.ForwardResponseMessage
forward_UserService_DeletePersonalAccessToken_0 = runtime.ForwardResponseMessage forward_UserService_DeletePersonalAccessToken_0 = runtime.ForwardResponseMessage
......
...@@ -31,6 +31,10 @@ const ( ...@@ -31,6 +31,10 @@ const (
UserService_GetUserSetting_FullMethodName = "/memos.api.v1.UserService/GetUserSetting" UserService_GetUserSetting_FullMethodName = "/memos.api.v1.UserService/GetUserSetting"
UserService_UpdateUserSetting_FullMethodName = "/memos.api.v1.UserService/UpdateUserSetting" UserService_UpdateUserSetting_FullMethodName = "/memos.api.v1.UserService/UpdateUserSetting"
UserService_ListUserSettings_FullMethodName = "/memos.api.v1.UserService/ListUserSettings" UserService_ListUserSettings_FullMethodName = "/memos.api.v1.UserService/ListUserSettings"
UserService_ListLinkedIdentities_FullMethodName = "/memos.api.v1.UserService/ListLinkedIdentities"
UserService_CreateLinkedIdentity_FullMethodName = "/memos.api.v1.UserService/CreateLinkedIdentity"
UserService_GetLinkedIdentity_FullMethodName = "/memos.api.v1.UserService/GetLinkedIdentity"
UserService_DeleteLinkedIdentity_FullMethodName = "/memos.api.v1.UserService/DeleteLinkedIdentity"
UserService_ListPersonalAccessTokens_FullMethodName = "/memos.api.v1.UserService/ListPersonalAccessTokens" UserService_ListPersonalAccessTokens_FullMethodName = "/memos.api.v1.UserService/ListPersonalAccessTokens"
UserService_CreatePersonalAccessToken_FullMethodName = "/memos.api.v1.UserService/CreatePersonalAccessToken" UserService_CreatePersonalAccessToken_FullMethodName = "/memos.api.v1.UserService/CreatePersonalAccessToken"
UserService_DeletePersonalAccessToken_FullMethodName = "/memos.api.v1.UserService/DeletePersonalAccessToken" UserService_DeletePersonalAccessToken_FullMethodName = "/memos.api.v1.UserService/DeletePersonalAccessToken"
...@@ -70,6 +74,14 @@ type UserServiceClient interface { ...@@ -70,6 +74,14 @@ type UserServiceClient interface {
UpdateUserSetting(ctx context.Context, in *UpdateUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error) UpdateUserSetting(ctx context.Context, in *UpdateUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error)
// ListUserSettings returns a list of user settings. // ListUserSettings returns a list of user settings.
ListUserSettings(ctx context.Context, in *ListUserSettingsRequest, opts ...grpc.CallOption) (*ListUserSettingsResponse, error) ListUserSettings(ctx context.Context, in *ListUserSettingsRequest, opts ...grpc.CallOption) (*ListUserSettingsResponse, error)
// ListLinkedIdentities returns a list of linked SSO identities for a user.
ListLinkedIdentities(ctx context.Context, in *ListLinkedIdentitiesRequest, opts ...grpc.CallOption) (*ListLinkedIdentitiesResponse, error)
// CreateLinkedIdentity links an SSO identity to the authenticated user.
CreateLinkedIdentity(ctx context.Context, in *CreateLinkedIdentityRequest, opts ...grpc.CallOption) (*LinkedIdentity, error)
// GetLinkedIdentity gets a linked SSO identity for a user.
GetLinkedIdentity(ctx context.Context, in *GetLinkedIdentityRequest, opts ...grpc.CallOption) (*LinkedIdentity, error)
// DeleteLinkedIdentity unlinks an SSO identity from a user.
DeleteLinkedIdentity(ctx context.Context, in *DeleteLinkedIdentityRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user. // ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens. // PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
ListPersonalAccessTokens(ctx context.Context, in *ListPersonalAccessTokensRequest, opts ...grpc.CallOption) (*ListPersonalAccessTokensResponse, error) ListPersonalAccessTokens(ctx context.Context, in *ListPersonalAccessTokensRequest, opts ...grpc.CallOption) (*ListPersonalAccessTokensResponse, error)
...@@ -212,6 +224,46 @@ func (c *userServiceClient) ListUserSettings(ctx context.Context, in *ListUserSe ...@@ -212,6 +224,46 @@ func (c *userServiceClient) ListUserSettings(ctx context.Context, in *ListUserSe
return out, nil return out, nil
} }
func (c *userServiceClient) ListLinkedIdentities(ctx context.Context, in *ListLinkedIdentitiesRequest, opts ...grpc.CallOption) (*ListLinkedIdentitiesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListLinkedIdentitiesResponse)
err := c.cc.Invoke(ctx, UserService_ListLinkedIdentities_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) CreateLinkedIdentity(ctx context.Context, in *CreateLinkedIdentityRequest, opts ...grpc.CallOption) (*LinkedIdentity, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(LinkedIdentity)
err := c.cc.Invoke(ctx, UserService_CreateLinkedIdentity_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) GetLinkedIdentity(ctx context.Context, in *GetLinkedIdentityRequest, opts ...grpc.CallOption) (*LinkedIdentity, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(LinkedIdentity)
err := c.cc.Invoke(ctx, UserService_GetLinkedIdentity_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) DeleteLinkedIdentity(ctx context.Context, in *DeleteLinkedIdentityRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, UserService_DeleteLinkedIdentity_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) ListPersonalAccessTokens(ctx context.Context, in *ListPersonalAccessTokensRequest, opts ...grpc.CallOption) (*ListPersonalAccessTokensResponse, error) { func (c *userServiceClient) ListPersonalAccessTokens(ctx context.Context, in *ListPersonalAccessTokensRequest, opts ...grpc.CallOption) (*ListPersonalAccessTokensResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListPersonalAccessTokensResponse) out := new(ListPersonalAccessTokensResponse)
...@@ -339,6 +391,14 @@ type UserServiceServer interface { ...@@ -339,6 +391,14 @@ type UserServiceServer interface {
UpdateUserSetting(context.Context, *UpdateUserSettingRequest) (*UserSetting, error) UpdateUserSetting(context.Context, *UpdateUserSettingRequest) (*UserSetting, error)
// ListUserSettings returns a list of user settings. // ListUserSettings returns a list of user settings.
ListUserSettings(context.Context, *ListUserSettingsRequest) (*ListUserSettingsResponse, error) ListUserSettings(context.Context, *ListUserSettingsRequest) (*ListUserSettingsResponse, error)
// ListLinkedIdentities returns a list of linked SSO identities for a user.
ListLinkedIdentities(context.Context, *ListLinkedIdentitiesRequest) (*ListLinkedIdentitiesResponse, error)
// CreateLinkedIdentity links an SSO identity to the authenticated user.
CreateLinkedIdentity(context.Context, *CreateLinkedIdentityRequest) (*LinkedIdentity, error)
// GetLinkedIdentity gets a linked SSO identity for a user.
GetLinkedIdentity(context.Context, *GetLinkedIdentityRequest) (*LinkedIdentity, error)
// DeleteLinkedIdentity unlinks an SSO identity from a user.
DeleteLinkedIdentity(context.Context, *DeleteLinkedIdentityRequest) (*emptypb.Empty, error)
// ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user. // ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
// PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens. // PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
ListPersonalAccessTokens(context.Context, *ListPersonalAccessTokensRequest) (*ListPersonalAccessTokensResponse, error) ListPersonalAccessTokens(context.Context, *ListPersonalAccessTokensRequest) (*ListPersonalAccessTokensResponse, error)
...@@ -404,6 +464,18 @@ func (UnimplementedUserServiceServer) UpdateUserSetting(context.Context, *Update ...@@ -404,6 +464,18 @@ func (UnimplementedUserServiceServer) UpdateUserSetting(context.Context, *Update
func (UnimplementedUserServiceServer) ListUserSettings(context.Context, *ListUserSettingsRequest) (*ListUserSettingsResponse, error) { func (UnimplementedUserServiceServer) ListUserSettings(context.Context, *ListUserSettingsRequest) (*ListUserSettingsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListUserSettings not implemented") return nil, status.Error(codes.Unimplemented, "method ListUserSettings not implemented")
} }
func (UnimplementedUserServiceServer) ListLinkedIdentities(context.Context, *ListLinkedIdentitiesRequest) (*ListLinkedIdentitiesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListLinkedIdentities not implemented")
}
func (UnimplementedUserServiceServer) CreateLinkedIdentity(context.Context, *CreateLinkedIdentityRequest) (*LinkedIdentity, error) {
return nil, status.Error(codes.Unimplemented, "method CreateLinkedIdentity not implemented")
}
func (UnimplementedUserServiceServer) GetLinkedIdentity(context.Context, *GetLinkedIdentityRequest) (*LinkedIdentity, error) {
return nil, status.Error(codes.Unimplemented, "method GetLinkedIdentity not implemented")
}
func (UnimplementedUserServiceServer) DeleteLinkedIdentity(context.Context, *DeleteLinkedIdentityRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteLinkedIdentity not implemented")
}
func (UnimplementedUserServiceServer) ListPersonalAccessTokens(context.Context, *ListPersonalAccessTokensRequest) (*ListPersonalAccessTokensResponse, error) { func (UnimplementedUserServiceServer) ListPersonalAccessTokens(context.Context, *ListPersonalAccessTokensRequest) (*ListPersonalAccessTokensResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListPersonalAccessTokens not implemented") return nil, status.Error(codes.Unimplemented, "method ListPersonalAccessTokens not implemented")
} }
...@@ -653,6 +725,78 @@ func _UserService_ListUserSettings_Handler(srv interface{}, ctx context.Context, ...@@ -653,6 +725,78 @@ func _UserService_ListUserSettings_Handler(srv interface{}, ctx context.Context,
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _UserService_ListLinkedIdentities_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListLinkedIdentitiesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).ListLinkedIdentities(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_ListLinkedIdentities_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).ListLinkedIdentities(ctx, req.(*ListLinkedIdentitiesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_CreateLinkedIdentity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateLinkedIdentityRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).CreateLinkedIdentity(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_CreateLinkedIdentity_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).CreateLinkedIdentity(ctx, req.(*CreateLinkedIdentityRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_GetLinkedIdentity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetLinkedIdentityRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).GetLinkedIdentity(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_GetLinkedIdentity_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).GetLinkedIdentity(ctx, req.(*GetLinkedIdentityRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_DeleteLinkedIdentity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteLinkedIdentityRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).DeleteLinkedIdentity(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_DeleteLinkedIdentity_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).DeleteLinkedIdentity(ctx, req.(*DeleteLinkedIdentityRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_ListPersonalAccessTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _UserService_ListPersonalAccessTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListPersonalAccessTokensRequest) in := new(ListPersonalAccessTokensRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
...@@ -884,6 +1028,22 @@ var UserService_ServiceDesc = grpc.ServiceDesc{ ...@@ -884,6 +1028,22 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListUserSettings", MethodName: "ListUserSettings",
Handler: _UserService_ListUserSettings_Handler, Handler: _UserService_ListUserSettings_Handler,
}, },
{
MethodName: "ListLinkedIdentities",
Handler: _UserService_ListLinkedIdentities_Handler,
},
{
MethodName: "CreateLinkedIdentity",
Handler: _UserService_CreateLinkedIdentity_Handler,
},
{
MethodName: "GetLinkedIdentity",
Handler: _UserService_GetLinkedIdentity_Handler,
},
{
MethodName: "DeleteLinkedIdentity",
Handler: _UserService_DeleteLinkedIdentity_Handler,
},
{ {
MethodName: "ListPersonalAccessTokens", MethodName: "ListPersonalAccessTokens",
Handler: _UserService_ListPersonalAccessTokens_Handler, Handler: _UserService_ListPersonalAccessTokens_Handler,
......
...@@ -1353,6 +1353,123 @@ paths: ...@@ -1353,6 +1353,123 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/Status' $ref: '#/components/schemas/Status'
/api/v1/users/{user}/linkedIdentities:
get:
tags:
- UserService
description: ListLinkedIdentities returns a list of linked SSO identities for a user.
operationId: UserService_ListLinkedIdentities
parameters:
- name: user
in: path
description: The user id.
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/ListLinkedIdentitiesResponse'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
post:
tags:
- UserService
description: CreateLinkedIdentity links an SSO identity to the authenticated user.
operationId: UserService_CreateLinkedIdentity
parameters:
- name: user
in: path
description: The user id.
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateLinkedIdentityRequest'
required: true
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/LinkedIdentity'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/users/{user}/linkedIdentities/{linkedIdentity}:
get:
tags:
- UserService
description: GetLinkedIdentity gets a linked SSO identity for a user.
operationId: UserService_GetLinkedIdentity
parameters:
- name: user
in: path
description: The user id.
required: true
schema:
type: string
- name: linkedIdentity
in: path
description: The linkedIdentity id.
required: true
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/LinkedIdentity'
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
delete:
tags:
- UserService
description: DeleteLinkedIdentity unlinks an SSO identity from a user.
operationId: UserService_DeleteLinkedIdentity
parameters:
- name: user
in: path
description: The user id.
required: true
schema:
type: string
- name: linkedIdentity
in: path
description: The linkedIdentity id.
required: true
schema:
type: string
responses:
"200":
description: OK
content: {}
default:
description: Default error response
content:
application/json:
schema:
$ref: '#/components/schemas/Status'
/api/v1/users/{user}/notifications: /api/v1/users/{user}/notifications:
get: get:
tags: tags:
...@@ -2269,6 +2386,33 @@ components: ...@@ -2269,6 +2386,33 @@ components:
}; };
// ... // ...
CreateLinkedIdentityRequest:
required:
- parent
- idpName
- code
- redirectUri
type: object
properties:
parent:
type: string
description: |-
Required. The parent user who owns the linked identity.
Format: users/{user}
idpName:
type: string
description: |-
Required. The identity provider to link.
Format: identity-providers/{uid}
code:
type: string
description: Required. The authorization code from the identity provider.
redirectUri:
type: string
description: Required. The redirect URI used in the OAuth flow.
codeVerifier:
type: string
description: Optional. The PKCE code verifier used in the OAuth flow.
CreatePersonalAccessTokenRequest: CreatePersonalAccessTokenRequest:
required: required:
- parent - parent
...@@ -2555,6 +2699,25 @@ components: ...@@ -2555,6 +2699,25 @@ components:
so a single entry like "project/.*" matches all tags under that prefix. so a single entry like "project/.*" matches all tags under that prefix.
Exact tag names are also valid (they are trivially valid regex patterns). Exact tag names are also valid (they are trivially valid regex patterns).
description: Tag metadata configuration. description: Tag metadata configuration.
LinkedIdentity:
type: object
properties:
name:
type: string
description: |-
The resource name of the linked identity.
Format: users/{user}/linkedIdentities/{linked_identity}
idpName:
readOnly: true
type: string
description: |-
The resource name of the identity provider.
Format: identity-providers/{uid}
externUid:
readOnly: true
type: string
description: The external user identifier from the identity provider.
description: LinkedIdentity represents an SSO identity linked to a user account.
ListAllUserStatsResponse: ListAllUserStatsResponse:
type: object type: object
properties: properties:
...@@ -2588,6 +2751,14 @@ components: ...@@ -2588,6 +2751,14 @@ components:
items: items:
$ref: '#/components/schemas/IdentityProvider' $ref: '#/components/schemas/IdentityProvider'
description: The list of identity providers. description: The list of identity providers.
ListLinkedIdentitiesResponse:
type: object
properties:
linkedIdentities:
type: array
items:
$ref: '#/components/schemas/LinkedIdentity'
description: The list of linked identities.
ListMemoAttachmentsResponse: ListMemoAttachmentsResponse:
type: object type: object
properties: properties:
......
...@@ -90,86 +90,13 @@ func (s *APIV1Service) SignIn(ctx context.Context, request *v1pb.SignInRequest) ...@@ -90,86 +90,13 @@ func (s *APIV1Service) SignIn(ctx context.Context, request *v1pb.SignInRequest)
existingUser = user existingUser = user
} else if ssoCredentials := request.GetSsoCredentials(); ssoCredentials != nil { } else if ssoCredentials := request.GetSsoCredentials(); ssoCredentials != nil {
// Authentication Method 2: SSO (OAuth2) authentication // Authentication Method 2: SSO (OAuth2) authentication
idpUID, err := ExtractIdentityProviderUIDFromName(ssoCredentials.IdpName) identityProvider, userInfo, err := s.resolveSSOIdentity(ctx, ssoCredentials.IdpName, ssoCredentials.Code, ssoCredentials.RedirectUri, ssoCredentials.CodeVerifier)
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid identity provider name: %v", err) return nil, err
} }
identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{ user, err := s.resolveSSOUser(ctx, nil, identityProvider, userInfo)
UID: &idpUID,
})
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get identity provider, error: %v", err) return nil, err
}
if identityProvider == nil {
return nil, status.Errorf(codes.InvalidArgument, "identity provider not found")
}
var userInfo *idp.IdentityProviderUserInfo
if identityProvider.Type == storepb.IdentityProvider_OAUTH2 {
oauth2IdentityProvider, err := oauth2.NewIdentityProvider(identityProvider.Config.GetOauth2Config())
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create oauth2 identity provider, error: %v", err)
}
// Pass code_verifier for PKCE support (empty string if not provided for backward compatibility)
token, err := oauth2IdentityProvider.ExchangeToken(ctx, ssoCredentials.RedirectUri, ssoCredentials.Code, ssoCredentials.CodeVerifier)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to exchange token, error: %v", err)
}
userInfo, err = oauth2IdentityProvider.UserInfo(token)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user info, error: %v", err)
}
}
identifierFilter := identityProvider.IdentifierFilter
if identifierFilter != "" {
identifierFilterRegex, err := regexp.Compile(identifierFilter)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to compile identifier filter regex, error: %v", err)
}
if !identifierFilterRegex.MatchString(userInfo.Identifier) {
return nil, status.Errorf(codes.PermissionDenied, "identifier %s is not allowed", userInfo.Identifier)
}
}
user, err := s.Store.GetUser(ctx, &store.FindUser{
Username: &userInfo.Identifier,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user, error: %v", err)
}
if user == nil {
// Check if the user is allowed to sign up.
instanceGeneralSetting, err := s.Store.GetInstanceGeneralSetting(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get instance general setting, error: %v", err)
}
if instanceGeneralSetting.DisallowUserRegistration {
return nil, status.Errorf(codes.PermissionDenied, "user registration is not allowed")
}
// Create a new user with the user info from the identity provider.
userCreate := &store.User{
Username: userInfo.Identifier,
// The new signup user should be normal user by default.
Role: store.RoleUser,
Nickname: userInfo.DisplayName,
Email: userInfo.Email,
AvatarURL: userInfo.AvatarURL,
}
password, err := util.RandomString(20)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate random password, error: %v", err)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate password hash, error: %v", err)
}
userCreate.PasswordHash = string(passwordHash)
user, err = s.Store.CreateUser(ctx, userCreate)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create user, error: %v", err)
}
} }
existingUser = user existingUser = user
} }
...@@ -193,6 +120,238 @@ func (s *APIV1Service) SignIn(ctx context.Context, request *v1pb.SignInRequest) ...@@ -193,6 +120,238 @@ func (s *APIV1Service) SignIn(ctx context.Context, request *v1pb.SignInRequest)
}, nil }, nil
} }
// resolveSSOUser resolves a local user from an external-identity subject, creating the
// linkage record (and a new local user if necessary) when first login is allowed.
//
// Lookup goes through the user_identity table so that userInfo.Identifier is never used
// as the local username key. On the miss path, a local user is created with a
// UUID-based local username (see deriveSSOUsername) and the (provider, extern_uid)
// linkage is inserted in the same flow. When currentUser is provided by a caller
// outside AuthService.SignIn, the lookup miss path binds the external identity to
// that existing user instead. If the linkage insert loses a race on the unique
// (provider, extern_uid) constraint, the winning linkage's user is loaded and
// checked against the current user.
func (s *APIV1Service) resolveSSOUser(ctx context.Context, currentUser *store.User, identityProvider *storepb.IdentityProvider, userInfo *idp.IdentityProviderUserInfo) (*store.User, error) {
provider := identityProvider.Uid
externUID := userInfo.Identifier
user, err := s.getLinkedSSOUser(ctx, provider, externUID)
if err != nil {
return nil, err
}
if user != nil {
if currentUser != nil && currentUser.ID != user.ID {
return nil, status.Errorf(codes.AlreadyExists, "identity provider account is already linked to another user")
}
return user, nil
}
if currentUser != nil {
return s.bindSSOIdentityToUser(ctx, currentUser, provider, externUID)
}
// Miss path: enforce the registration gate before creating anything.
instanceGeneralSetting, err := s.Store.GetInstanceGeneralSetting(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get instance general setting, error: %v", err)
}
if instanceGeneralSetting.DisallowUserRegistration {
return nil, status.Errorf(codes.PermissionDenied, "user registration is not allowed")
}
password, err := util.RandomString(20)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate random password, error: %v", err)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to generate password hash, error: %v", err)
}
username, err := deriveSSOUsername()
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to derive username, error: %v", err)
}
user, err = s.Store.CreateUser(ctx, &store.User{
Username: username,
Role: store.RoleUser,
Nickname: userInfo.DisplayName,
Email: userInfo.Email,
AvatarURL: userInfo.AvatarURL,
PasswordHash: string(passwordHash),
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to create user, error: %v", err)
}
if _, err := s.Store.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: provider,
ExternUID: externUID,
}); err != nil {
// Best-effort cleanup: the provisional user row has no linkage and should not remain.
_ = s.Store.DeleteUser(ctx, &store.DeleteUser{ID: user.ID})
if isUniqueConstraintViolation(err) {
// Concurrent first login won the race; load the winning linkage's user.
winner, getErr := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
Provider: &provider,
ExternUID: &externUID,
})
if getErr != nil {
return nil, status.Errorf(codes.Internal, "failed to reload user identity after race, error: %v", getErr)
}
if winner == nil {
return nil, status.Errorf(codes.Internal, "user identity conflict reported but no winning row found")
}
winnerUser, getErr := s.Store.GetUser(ctx, &store.FindUser{ID: &winner.UserID})
if getErr != nil {
return nil, status.Errorf(codes.Internal, "failed to get user after race, error: %v", getErr)
}
if winnerUser == nil {
return nil, status.Errorf(codes.Internal, "linked user %d not found after race", winner.UserID)
}
return winnerUser, nil
}
return nil, status.Errorf(codes.Internal, "failed to create user identity, error: %v", err)
}
return user, nil
}
func (s *APIV1Service) resolveSSOIdentity(ctx context.Context, idpName, code, redirectURI, codeVerifier string) (*storepb.IdentityProvider, *idp.IdentityProviderUserInfo, error) {
idpUID, err := ExtractIdentityProviderUIDFromName(idpName)
if err != nil {
return nil, nil, status.Errorf(codes.InvalidArgument, "invalid identity provider name: %v", err)
}
identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{
UID: &idpUID,
})
if err != nil {
return nil, nil, status.Errorf(codes.Internal, "failed to get identity provider, error: %v", err)
}
if identityProvider == nil {
return nil, nil, status.Errorf(codes.InvalidArgument, "identity provider not found")
}
var userInfo *idp.IdentityProviderUserInfo
if identityProvider.Type == storepb.IdentityProvider_OAUTH2 {
oauth2IdentityProvider, err := oauth2.NewIdentityProvider(identityProvider.Config.GetOauth2Config())
if err != nil {
return nil, nil, status.Errorf(codes.Internal, "failed to create oauth2 identity provider, error: %v", err)
}
// Pass code_verifier for PKCE support (empty string if not provided for backward compatibility)
token, err := oauth2IdentityProvider.ExchangeToken(ctx, redirectURI, code, codeVerifier)
if err != nil {
return nil, nil, status.Errorf(codes.Internal, "failed to exchange token, error: %v", err)
}
userInfo, err = oauth2IdentityProvider.UserInfo(token)
if err != nil {
return nil, nil, status.Errorf(codes.Internal, "failed to get user info, error: %v", err)
}
}
identifierFilter := identityProvider.IdentifierFilter
if identifierFilter != "" {
identifierFilterRegex, err := regexp.Compile(identifierFilter)
if err != nil {
return nil, nil, status.Errorf(codes.Internal, "failed to compile identifier filter regex, error: %v", err)
}
if !identifierFilterRegex.MatchString(userInfo.Identifier) {
return nil, nil, status.Errorf(codes.PermissionDenied, "identifier %s is not allowed", userInfo.Identifier)
}
}
return identityProvider, userInfo, nil
}
func (s *APIV1Service) getLinkedSSOUser(ctx context.Context, provider, externUID string) (*store.User, error) {
identity, err := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
Provider: &provider,
ExternUID: &externUID,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user identity, error: %v", err)
}
if identity == nil {
return nil, nil
}
user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &identity.UserID})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user, error: %v", err)
}
if user == nil {
return nil, status.Errorf(codes.Internal, "linked user %d not found for identity %d", identity.UserID, identity.ID)
}
return user, nil
}
func (s *APIV1Service) bindSSOIdentityToUser(ctx context.Context, currentUser *store.User, provider, externUID string) (*store.User, error) {
existingForProvider, err := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
UserID: &currentUser.ID,
Provider: &provider,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get existing linked identity, error: %v", err)
}
if existingForProvider != nil {
if existingForProvider.ExternUID == externUID {
return currentUser, nil
}
return nil, status.Errorf(codes.AlreadyExists, "identity provider is already linked to another external account for this user")
}
if _, err := s.Store.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: currentUser.ID,
Provider: provider,
ExternUID: externUID,
}); err != nil {
if isUniqueConstraintViolation(err) {
winner, getErr := s.getLinkedSSOUser(ctx, provider, externUID)
if getErr != nil {
return nil, getErr
}
if winner != nil {
if winner.ID != currentUser.ID {
return nil, status.Errorf(codes.AlreadyExists, "identity provider account is already linked to another user")
}
return currentUser, nil
}
existingForProvider, getErr := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
UserID: &currentUser.ID,
Provider: &provider,
})
if getErr != nil {
return nil, status.Errorf(codes.Internal, "failed to reload linked identity after race, error: %v", getErr)
}
if existingForProvider != nil {
if existingForProvider.ExternUID == externUID {
return currentUser, nil
}
return nil, status.Errorf(codes.AlreadyExists, "identity provider is already linked to another external account for this user")
}
return nil, status.Errorf(codes.Internal, "user identity conflict reported but no winning row found")
}
return nil, status.Errorf(codes.Internal, "failed to create user identity, error: %v", err)
}
return currentUser, nil
}
// isUniqueConstraintViolation matches the driver-specific error messages that each
// supported backend emits when any UNIQUE constraint rejects an insert. Callers
// disambiguate which constraint was hit from the insertion context (e.g. inserting
// a user_identity row can only violate UNIQUE(provider, extern_uid); inserting a
// user row can only violate UNIQUE(username)). Matches the pattern used in
// memo_service.go for the memo UID unique check.
func isUniqueConstraintViolation(err error) bool {
if err == nil {
return false
}
msg := err.Error()
return strings.Contains(msg, "UNIQUE constraint failed") ||
strings.Contains(msg, "duplicate key") ||
strings.Contains(msg, "Duplicate entry")
}
// doSignIn performs the actual sign-in operation by creating a session and setting the cookie. // doSignIn performs the actual sign-in operation by creating a session and setting the cookie.
// //
// This function: // This function:
......
...@@ -112,11 +112,9 @@ func (s *ConnectServiceHandler) UpdateUser(ctx context.Context, req *connect.Req ...@@ -112,11 +112,9 @@ func (s *ConnectServiceHandler) UpdateUser(ctx context.Context, req *connect.Req
} }
func (s *ConnectServiceHandler) DeleteUser(ctx context.Context, req *connect.Request[v1pb.DeleteUserRequest]) (*connect.Response[emptypb.Empty], error) { func (s *ConnectServiceHandler) DeleteUser(ctx context.Context, req *connect.Request[v1pb.DeleteUserRequest]) (*connect.Response[emptypb.Empty], error) {
resp, err := s.APIV1Service.DeleteUser(ctx, req.Msg) return connectWithHeaderCarrier(ctx, func(ctx context.Context) (*emptypb.Empty, error) {
if err != nil { return s.APIV1Service.DeleteUser(ctx, req.Msg)
return nil, convertGRPCError(err) })
}
return connect.NewResponse(resp), nil
} }
func (s *ConnectServiceHandler) ListAllUserStats(ctx context.Context, req *connect.Request[v1pb.ListAllUserStatsRequest]) (*connect.Response[v1pb.ListAllUserStatsResponse], error) { func (s *ConnectServiceHandler) ListAllUserStats(ctx context.Context, req *connect.Request[v1pb.ListAllUserStatsRequest]) (*connect.Response[v1pb.ListAllUserStatsResponse], error) {
...@@ -159,6 +157,38 @@ func (s *ConnectServiceHandler) ListUserSettings(ctx context.Context, req *conne ...@@ -159,6 +157,38 @@ func (s *ConnectServiceHandler) ListUserSettings(ctx context.Context, req *conne
return connect.NewResponse(resp), nil return connect.NewResponse(resp), nil
} }
func (s *ConnectServiceHandler) ListLinkedIdentities(ctx context.Context, req *connect.Request[v1pb.ListLinkedIdentitiesRequest]) (*connect.Response[v1pb.ListLinkedIdentitiesResponse], error) {
resp, err := s.APIV1Service.ListLinkedIdentities(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) CreateLinkedIdentity(ctx context.Context, req *connect.Request[v1pb.CreateLinkedIdentityRequest]) (*connect.Response[v1pb.LinkedIdentity], error) {
resp, err := s.APIV1Service.CreateLinkedIdentity(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) GetLinkedIdentity(ctx context.Context, req *connect.Request[v1pb.GetLinkedIdentityRequest]) (*connect.Response[v1pb.LinkedIdentity], error) {
resp, err := s.APIV1Service.GetLinkedIdentity(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) DeleteLinkedIdentity(ctx context.Context, req *connect.Request[v1pb.DeleteLinkedIdentityRequest]) (*connect.Response[emptypb.Empty], error) {
resp, err := s.APIV1Service.DeleteLinkedIdentity(ctx, req.Msg)
if err != nil {
return nil, convertGRPCError(err)
}
return connect.NewResponse(resp), nil
}
func (s *ConnectServiceHandler) ListPersonalAccessTokens(ctx context.Context, req *connect.Request[v1pb.ListPersonalAccessTokensRequest]) (*connect.Response[v1pb.ListPersonalAccessTokensResponse], error) { func (s *ConnectServiceHandler) ListPersonalAccessTokens(ctx context.Context, req *connect.Request[v1pb.ListPersonalAccessTokensRequest]) (*connect.Response[v1pb.ListPersonalAccessTokensResponse], error) {
resp, err := s.APIV1Service.ListPersonalAccessTokens(ctx, req.Msg) resp, err := s.APIV1Service.ListPersonalAccessTokens(ctx, req.Msg)
if err != nil { if err != nil {
......
package v1
import (
"github.com/pkg/errors"
"github.com/usememos/memos/internal/util"
)
// deriveSSOUsername produces the local username for a new SSO-created user.
//
// The current policy is to use a standard UUID string directly. This keeps the
// username independent of IdP profile fields and avoids availability probes or
// retry loops around concurrent first-time logins.
func deriveSSOUsername() (string, error) {
username := util.GenUUID()
if err := validateUsername(username); err != nil {
return "", errors.Wrap(err, "generated UUID did not satisfy username constraints")
}
return username, nil
}
package test
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
storepb "github.com/usememos/memos/proto/gen/store"
apiv1 "github.com/usememos/memos/server/router/api/v1"
"github.com/usememos/memos/store"
)
func TestCreateLinkedIdentityBindsCurrentUser(t *testing.T) {
t.Parallel()
ts := NewTestService(t)
defer ts.Cleanup()
ctx := context.Background()
currentUser, err := ts.CreateRegularUser(ctx, "alice")
require.NoError(t, err)
mockIDP := newMockOAuthServer(t, "bind-code", "bind-access-token", map[string]any{
"sub": "google-sub-1",
"name": "Alice Example",
"email": "alice@example.com",
})
defer mockIDP.Close()
idpName := createTestingOAuthIdentityProvider(ctx, t, ts, mockIDP.URL, "google-bind")
beforeUsers, err := ts.Store.ListUsers(ctx, &store.FindUser{})
require.NoError(t, err)
authCtx := ts.CreateUserContext(apiv1.WithHeaderCarrier(ctx), currentUser.ID)
response, err := ts.Service.CreateLinkedIdentity(authCtx, &v1pb.CreateLinkedIdentityRequest{
Parent: apiv1.BuildUserName(currentUser.Username),
IdpName: idpName,
Code: "bind-code",
RedirectUri: "http://localhost:8080/auth/callback",
})
require.NoError(t, err)
require.NotNil(t, response)
require.Equal(t, apiv1.BuildUserName(currentUser.Username)+"/linkedIdentities/google-bind", response.Name)
require.Equal(t, apiv1.IdentityProviderNamePrefix+"google-bind", response.IdpName)
require.Equal(t, "google-sub-1", response.ExternUid)
afterUsers, err := ts.Store.ListUsers(ctx, &store.FindUser{})
require.NoError(t, err)
require.Len(t, afterUsers, len(beforeUsers))
provider := "google-bind"
externUID := "google-sub-1"
identity, err := ts.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
Provider: &provider,
ExternUID: &externUID,
})
require.NoError(t, err)
require.NotNil(t, identity)
require.Equal(t, currentUser.ID, identity.UserID)
}
func TestCreateLinkedIdentityRejectsBindingIdentityLinkedToAnotherUser(t *testing.T) {
t.Parallel()
ts := NewTestService(t)
defer ts.Cleanup()
ctx := context.Background()
owner, err := ts.CreateRegularUser(ctx, "owner")
require.NoError(t, err)
binder, err := ts.CreateRegularUser(ctx, "binder")
require.NoError(t, err)
mockIDP := newMockOAuthServer(t, "conflict-code", "conflict-access-token", map[string]any{
"sub": "google-sub-2",
"name": "Conflict Example",
"email": "conflict@example.com",
})
defer mockIDP.Close()
idpName := createTestingOAuthIdentityProvider(ctx, t, ts, mockIDP.URL, "google-conflict")
_, err = ts.Store.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: owner.ID,
Provider: "google-conflict",
ExternUID: "google-sub-2",
})
require.NoError(t, err)
authCtx := ts.CreateUserContext(apiv1.WithHeaderCarrier(ctx), binder.ID)
_, err = ts.Service.CreateLinkedIdentity(authCtx, &v1pb.CreateLinkedIdentityRequest{
Parent: apiv1.BuildUserName(binder.Username),
IdpName: idpName,
Code: "conflict-code",
RedirectUri: "http://localhost:8080/auth/callback",
})
require.Error(t, err)
require.Equal(t, codes.AlreadyExists, status.Code(err))
}
func TestListAndDeleteLinkedIdentities(t *testing.T) {
t.Parallel()
ts := NewTestService(t)
defer ts.Cleanup()
ctx := context.Background()
currentUser, err := ts.CreateRegularUser(ctx, "alice")
require.NoError(t, err)
_, err = ts.Store.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: currentUser.ID,
Provider: "google",
ExternUID: "alice@gmail.com",
})
require.NoError(t, err)
authCtx := ts.CreateUserContext(ctx, currentUser.ID)
listResp, err := ts.Service.ListLinkedIdentities(authCtx, &v1pb.ListLinkedIdentitiesRequest{
Parent: apiv1.BuildUserName(currentUser.Username),
})
require.NoError(t, err)
require.Len(t, listResp.LinkedIdentities, 1)
linkedIdentityName := apiv1.BuildUserName(currentUser.Username) + "/linkedIdentities/google"
require.Equal(t, linkedIdentityName, listResp.LinkedIdentities[0].Name)
require.Equal(t, apiv1.IdentityProviderNamePrefix+"google", listResp.LinkedIdentities[0].IdpName)
require.Equal(t, "alice@gmail.com", listResp.LinkedIdentities[0].ExternUid)
got, err := ts.Service.GetLinkedIdentity(authCtx, &v1pb.GetLinkedIdentityRequest{
Name: linkedIdentityName,
})
require.NoError(t, err)
require.Equal(t, linkedIdentityName, got.Name)
require.Equal(t, apiv1.IdentityProviderNamePrefix+"google", got.IdpName)
require.Equal(t, "alice@gmail.com", got.ExternUid)
_, err = ts.Service.DeleteLinkedIdentity(authCtx, &v1pb.DeleteLinkedIdentityRequest{
Name: linkedIdentityName,
})
require.NoError(t, err)
listResp, err = ts.Service.ListLinkedIdentities(authCtx, &v1pb.ListLinkedIdentitiesRequest{
Parent: apiv1.BuildUserName(currentUser.Username),
})
require.NoError(t, err)
require.Empty(t, listResp.LinkedIdentities)
}
func TestCreateLinkedIdentityRejectsSecondIdentityForSameProvider(t *testing.T) {
t.Parallel()
ts := NewTestService(t)
defer ts.Cleanup()
ctx := context.Background()
currentUser, err := ts.CreateRegularUser(ctx, "alice")
require.NoError(t, err)
_, err = ts.Store.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: currentUser.ID,
Provider: "google-provider",
ExternUID: "google-sub-1",
})
require.NoError(t, err)
mockIDP := newMockOAuthServer(t, "second-code", "second-access-token", map[string]any{
"sub": "google-sub-2",
"name": "Alice Example",
"email": "alice@example.com",
})
defer mockIDP.Close()
idpName := createTestingOAuthIdentityProvider(ctx, t, ts, mockIDP.URL, "google-provider")
authCtx := ts.CreateUserContext(apiv1.WithHeaderCarrier(ctx), currentUser.ID)
_, err = ts.Service.CreateLinkedIdentity(authCtx, &v1pb.CreateLinkedIdentityRequest{
Parent: apiv1.BuildUserName(currentUser.Username),
IdpName: idpName,
Code: "second-code",
RedirectUri: "http://localhost:8080/auth/callback",
})
require.Error(t, err)
require.Equal(t, codes.AlreadyExists, status.Code(err))
}
func createTestingOAuthIdentityProvider(ctx context.Context, t *testing.T, ts *TestService, serverURL, uid string) string {
t.Helper()
idp, err := ts.Store.CreateIdentityProvider(ctx, &storepb.IdentityProvider{
Uid: uid,
Name: "Google",
Type: storepb.IdentityProvider_OAUTH2,
Config: &storepb.IdentityProviderConfig{
Config: &storepb.IdentityProviderConfig_Oauth2Config{
Oauth2Config: &storepb.OAuth2Config{
ClientId: "test-client-id",
ClientSecret: "test-client-secret",
AuthUrl: serverURL + "/oauth2/authorize",
TokenUrl: serverURL + "/oauth2/token",
UserInfoUrl: serverURL + "/oauth2/userinfo",
FieldMapping: &storepb.FieldMapping{
Identifier: "sub",
DisplayName: "name",
Email: "email",
},
},
},
},
})
require.NoError(t, err)
return apiv1.IdentityProviderNamePrefix + idp.Uid
}
func newMockOAuthServer(t *testing.T, code, accessToken string, userInfo map[string]any) *httptest.Server {
t.Helper()
userInfoBytes, err := json.Marshal(userInfo)
require.NoError(t, err)
mux := http.NewServeMux()
mux.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
values, err := url.ParseQuery(string(body))
require.NoError(t, err)
require.Equal(t, code, values.Get("code"))
require.Equal(t, "authorization_code", values.Get("grant_type"))
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(map[string]any{
"access_token": accessToken,
"token_type": "Bearer",
"expires_in": 3600,
})
require.NoError(t, err)
})
mux.HandleFunc("/oauth2/userinfo", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, err := w.Write(userInfoBytes)
require.NoError(t, err)
})
return httptest.NewServer(mux)
}
package test
import (
"context"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
v1pb "github.com/usememos/memos/proto/gen/api/v1"
storepb "github.com/usememos/memos/proto/gen/store"
apiv1 "github.com/usememos/memos/server/router/api/v1"
"github.com/usememos/memos/store"
)
func TestDeleteUserSelfDeleteCleansAccountDataAndAuthCookies(t *testing.T) {
t.Parallel()
ts := NewTestService(t)
defer ts.Cleanup()
ctx := context.Background()
user, err := ts.CreateRegularUser(ctx, "alice")
require.NoError(t, err)
_, err = ts.Store.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "google",
ExternUID: "alice-google-sub",
})
require.NoError(t, err)
err = ts.Store.AddUserRefreshToken(ctx, user.ID, &storepb.RefreshTokensUserSetting_RefreshToken{
TokenId: "refresh-token-id",
ExpiresAt: timestamppb.New(time.Now().Add(time.Hour)),
CreatedAt: timestamppb.Now(),
})
require.NoError(t, err)
headerCtx := apiv1.WithHeaderCarrier(ctx)
authCtx := ts.CreateUserContext(headerCtx, user.ID)
_, err = ts.Service.DeleteUser(authCtx, &v1pb.DeleteUserRequest{
Name: apiv1.BuildUserName(user.Username),
})
require.NoError(t, err)
deletedUser, err := ts.Store.GetUser(ctx, &store.FindUser{ID: &user.ID})
require.NoError(t, err)
require.Nil(t, deletedUser)
identities, err := ts.Store.ListUserIdentities(ctx, &store.FindUserIdentity{UserID: &user.ID})
require.NoError(t, err)
require.Empty(t, identities)
refreshSetting, err := ts.Store.GetUserSetting(ctx, &store.FindUserSetting{
UserID: &user.ID,
Key: storepb.UserSetting_REFRESH_TOKENS,
})
require.NoError(t, err)
require.Nil(t, refreshSetting)
carrier := apiv1.GetHeaderCarrier(authCtx)
require.NotNil(t, carrier)
require.Contains(t, strings.ToLower(carrier.Get("Set-Cookie")), "memos_refresh=")
}
...@@ -335,12 +335,29 @@ func (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserR ...@@ -335,12 +335,29 @@ func (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserR
if currentUser.ID != userID && currentUser.Role != store.RoleAdmin { if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
return nil, status.Errorf(codes.PermissionDenied, "permission denied") return nil, status.Errorf(codes.PermissionDenied, "permission denied")
} }
isSelfDelete := currentUser.ID == userID
if err := s.Store.DeleteUserIdentities(ctx, &store.DeleteUserIdentity{
UserID: &userID,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete user identities: %v", err)
}
if err := s.Store.DeleteUserSettings(ctx, &store.DeleteUserSetting{
UserID: &userID,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete user settings: %v", err)
}
if err := s.Store.DeleteUser(ctx, &store.DeleteUser{ if err := s.Store.DeleteUser(ctx, &store.DeleteUser{
ID: user.ID, ID: user.ID,
}); err != nil { }); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete user: %v", err) return nil, status.Errorf(codes.Internal, "failed to delete user: %v", err)
} }
if isSelfDelete {
if err := s.clearAuthCookies(ctx); err != nil {
slog.Warn("failed to clear auth cookies after self delete", "user_id", userID, "error", err)
}
}
return &emptypb.Empty{}, nil return &emptypb.Empty{}, nil
} }
...@@ -390,6 +407,27 @@ func (s *APIV1Service) resolveUserAndWebhookIDFromName(ctx context.Context, name ...@@ -390,6 +407,27 @@ func (s *APIV1Service) resolveUserAndWebhookIDFromName(ctx context.Context, name
return user, parts[3], nil return user, parts[3], nil
} }
func (s *APIV1Service) resolveUserAndLinkedIdentityProviderFromName(ctx context.Context, name string) (*store.User, string, error) {
parts := strings.Split(name, "/")
if len(parts) != 4 || parts[0] != "users" || parts[2] != "linkedIdentities" {
return nil, "", errors.Errorf("invalid linked identity name: %s", name)
}
user, err := s.resolveUserFromName(ctx, BuildUserName(parts[1]))
if err != nil {
return nil, "", err
}
return user, parts[3], nil
}
func convertLinkedIdentityFromStore(user *store.User, identity *store.UserIdentity) *v1pb.LinkedIdentity {
return &v1pb.LinkedIdentity{
Name: fmt.Sprintf("%s/linkedIdentities/%s", BuildUserName(user.Username), identity.Provider),
IdpName: IdentityProviderNamePrefix + identity.Provider,
ExternUid: identity.ExternUID,
}
}
func (s *APIV1Service) resolveUserAndNotificationIDFromName(ctx context.Context, name string) (*store.User, int32, error) { func (s *APIV1Service) resolveUserAndNotificationIDFromName(ctx context.Context, name string) (*store.User, int32, error) {
parts := strings.Split(name, "/") parts := strings.Split(name, "/")
if len(parts) != 4 || parts[0] != "users" || parts[2] != "notifications" { if len(parts) != 4 || parts[0] != "users" || parts[2] != "notifications" {
...@@ -597,6 +635,141 @@ func (s *APIV1Service) ListUserSettings(ctx context.Context, request *v1pb.ListU ...@@ -597,6 +635,141 @@ func (s *APIV1Service) ListUserSettings(ctx context.Context, request *v1pb.ListU
return response, nil return response, nil
} }
func (s *APIV1Service) ListLinkedIdentities(ctx context.Context, request *v1pb.ListLinkedIdentitiesRequest) (*v1pb.ListLinkedIdentitiesResponse, error) {
user, err := s.resolveUserFromName(ctx, request.Parent)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err)
}
userID := user.ID
claims := auth.GetUserClaims(ctx)
if claims == nil || claims.UserID != userID {
currentUser, _ := s.fetchCurrentUser(ctx)
if currentUser == nil || (currentUser.ID != userID && currentUser.Role != store.RoleAdmin) {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
}
identities, err := s.Store.ListUserIdentities(ctx, &store.FindUserIdentity{UserID: &userID})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list linked identities: %v", err)
}
response := &v1pb.ListLinkedIdentitiesResponse{
LinkedIdentities: []*v1pb.LinkedIdentity{},
}
for _, identity := range identities {
response.LinkedIdentities = append(response.LinkedIdentities, convertLinkedIdentityFromStore(user, identity))
}
return response, nil
}
func (s *APIV1Service) CreateLinkedIdentity(ctx context.Context, request *v1pb.CreateLinkedIdentityRequest) (*v1pb.LinkedIdentity, error) {
user, err := s.resolveUserFromName(ctx, request.Parent)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err)
}
currentUser, err := s.fetchCurrentUser(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
}
if currentUser == nil {
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
}
if currentUser.ID != user.ID {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
identityProvider, userInfo, err := s.resolveSSOIdentity(ctx, request.IdpName, request.Code, request.RedirectUri, request.CodeVerifier)
if err != nil {
return nil, err
}
provider := identityProvider.Uid
externUID := userInfo.Identifier
if _, err := s.bindSSOIdentityToUser(ctx, currentUser, provider, externUID); err != nil {
return nil, err
}
identity, err := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
UserID: &currentUser.ID,
Provider: &provider,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get linked identity: %v", err)
}
if identity == nil {
return nil, status.Errorf(codes.Internal, "linked identity not found after creation")
}
return convertLinkedIdentityFromStore(user, identity), nil
}
func (s *APIV1Service) GetLinkedIdentity(ctx context.Context, request *v1pb.GetLinkedIdentityRequest) (*v1pb.LinkedIdentity, error) {
user, provider, err := s.resolveUserAndLinkedIdentityProviderFromName(ctx, request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid linked identity name: %v", err)
}
userID := user.ID
claims := auth.GetUserClaims(ctx)
if claims == nil || claims.UserID != userID {
currentUser, _ := s.fetchCurrentUser(ctx)
if currentUser == nil || (currentUser.ID != userID && currentUser.Role != store.RoleAdmin) {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
}
identity, err := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
UserID: &userID,
Provider: &provider,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get linked identity: %v", err)
}
if identity == nil {
return nil, status.Errorf(codes.NotFound, "linked identity not found")
}
return convertLinkedIdentityFromStore(user, identity), nil
}
func (s *APIV1Service) DeleteLinkedIdentity(ctx context.Context, request *v1pb.DeleteLinkedIdentityRequest) (*emptypb.Empty, error) {
user, provider, err := s.resolveUserAndLinkedIdentityProviderFromName(ctx, request.Name)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid linked identity name: %v", err)
}
userID := user.ID
claims := auth.GetUserClaims(ctx)
if claims == nil || claims.UserID != userID {
currentUser, _ := s.fetchCurrentUser(ctx)
if currentUser == nil || (currentUser.ID != userID && currentUser.Role != store.RoleAdmin) {
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
}
}
existing, err := s.Store.GetUserIdentity(ctx, &store.FindUserIdentity{
UserID: &userID,
Provider: &provider,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get linked identity: %v", err)
}
if existing == nil {
return nil, status.Errorf(codes.NotFound, "linked identity not found")
}
if err := s.Store.DeleteUserIdentities(ctx, &store.DeleteUserIdentity{
UserID: &userID,
Provider: &provider,
}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete linked identity: %v", err)
}
return &emptypb.Empty{}, nil
}
// ListPersonalAccessTokens retrieves all Personal Access Tokens (PATs) for a user. // ListPersonalAccessTokens retrieves all Personal Access Tokens (PATs) for a user.
// //
// Personal Access Tokens are used for: // Personal Access Tokens are used for:
......
package mysql
import (
"context"
"strings"
"github.com/pkg/errors"
"github.com/usememos/memos/store"
)
func (d *DB) CreateUserIdentity(ctx context.Context, create *store.UserIdentity) (*store.UserIdentity, error) {
stmt := "INSERT INTO `user_identity` (`user_id`, `provider`, `extern_uid`) VALUES (?, ?, ?)"
result, err := d.db.ExecContext(ctx, stmt, create.UserID, create.Provider, create.ExternUID)
if err != nil {
return nil, err
}
rawID, err := result.LastInsertId()
if err != nil {
return nil, err
}
id := int32(rawID)
list, err := d.ListUserIdentities(ctx, &store.FindUserIdentity{ID: &id})
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, errors.Errorf("failed to create user identity")
}
return list[0], nil
}
func (d *DB) ListUserIdentities(ctx context.Context, find *store.FindUserIdentity) ([]*store.UserIdentity, error) {
where, args := []string{"1 = 1"}, []any{}
if find.ID != nil {
where, args = append(where, "`id` = ?"), append(args, *find.ID)
}
if find.UserID != nil {
where, args = append(where, "`user_id` = ?"), append(args, *find.UserID)
}
if find.Provider != nil {
where, args = append(where, "`provider` = ?"), append(args, *find.Provider)
}
if find.ExternUID != nil {
where, args = append(where, "`extern_uid` = ?"), append(args, *find.ExternUID)
}
rows, err := d.db.QueryContext(ctx, `
SELECT
id,
user_id,
provider,
extern_uid,
created_ts,
updated_ts
FROM user_identity
WHERE `+strings.Join(where, " AND ")+`
ORDER BY id ASC`,
args...,
)
if err != nil {
return nil, err
}
defer rows.Close()
list := []*store.UserIdentity{}
for rows.Next() {
ui := &store.UserIdentity{}
if err := rows.Scan(
&ui.ID,
&ui.UserID,
&ui.Provider,
&ui.ExternUID,
&ui.CreatedTs,
&ui.UpdatedTs,
); err != nil {
return nil, err
}
list = append(list, ui)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (d *DB) DeleteUserIdentities(ctx context.Context, delete *store.DeleteUserIdentity) error {
where, args := []string{"1 = 1"}, []any{}
if delete.ID != nil {
where, args = append(where, "`id` = ?"), append(args, *delete.ID)
}
if delete.UserID != nil {
where, args = append(where, "`user_id` = ?"), append(args, *delete.UserID)
}
if delete.Provider != nil {
where, args = append(where, "`provider` = ?"), append(args, *delete.Provider)
}
if _, err := d.db.ExecContext(ctx, "DELETE FROM `user_identity` WHERE "+strings.Join(where, " AND "), args...); err != nil {
return err
}
return nil
}
...@@ -57,6 +57,22 @@ func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting) ...@@ -57,6 +57,22 @@ func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting)
return userSettingList, nil return userSettingList, nil
} }
func (d *DB) DeleteUserSettings(ctx context.Context, delete *store.DeleteUserSetting) error {
where, args := []string{"1 = 1"}, []any{}
if v := delete.Key; v != storepb.UserSetting_KEY_UNSPECIFIED {
where, args = append(where, "`key` = ?"), append(args, v.String())
}
if v := delete.UserID; v != nil {
where, args = append(where, "`user_id` = ?"), append(args, *v)
}
if _, err := d.db.ExecContext(ctx, "DELETE FROM `user_setting` WHERE "+strings.Join(where, " AND "), args...); err != nil {
return err
}
return nil
}
func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) { func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) {
query := ` query := `
SELECT SELECT
......
package postgres
import (
"context"
"strings"
"github.com/usememos/memos/store"
)
func (d *DB) CreateUserIdentity(ctx context.Context, create *store.UserIdentity) (*store.UserIdentity, error) {
stmt := "INSERT INTO user_identity (user_id, provider, extern_uid) VALUES (" + placeholders(3) + ") RETURNING id, created_ts, updated_ts"
if err := d.db.QueryRowContext(ctx, stmt, create.UserID, create.Provider, create.ExternUID).Scan(
&create.ID,
&create.CreatedTs,
&create.UpdatedTs,
); err != nil {
return nil, err
}
return create, nil
}
func (d *DB) ListUserIdentities(ctx context.Context, find *store.FindUserIdentity) ([]*store.UserIdentity, error) {
where, args := []string{"1 = 1"}, []any{}
if find.ID != nil {
where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *find.ID)
}
if find.UserID != nil {
where, args = append(where, "user_id = "+placeholder(len(args)+1)), append(args, *find.UserID)
}
if find.Provider != nil {
where, args = append(where, "provider = "+placeholder(len(args)+1)), append(args, *find.Provider)
}
if find.ExternUID != nil {
where, args = append(where, "extern_uid = "+placeholder(len(args)+1)), append(args, *find.ExternUID)
}
rows, err := d.db.QueryContext(ctx, `
SELECT
id,
user_id,
provider,
extern_uid,
created_ts,
updated_ts
FROM user_identity
WHERE `+strings.Join(where, " AND ")+`
ORDER BY id ASC`,
args...,
)
if err != nil {
return nil, err
}
defer rows.Close()
list := []*store.UserIdentity{}
for rows.Next() {
ui := &store.UserIdentity{}
if err := rows.Scan(
&ui.ID,
&ui.UserID,
&ui.Provider,
&ui.ExternUID,
&ui.CreatedTs,
&ui.UpdatedTs,
); err != nil {
return nil, err
}
list = append(list, ui)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (d *DB) DeleteUserIdentities(ctx context.Context, delete *store.DeleteUserIdentity) error {
where, args := []string{"1 = 1"}, []any{}
if delete.ID != nil {
where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *delete.ID)
}
if delete.UserID != nil {
where, args = append(where, "user_id = "+placeholder(len(args)+1)), append(args, *delete.UserID)
}
if delete.Provider != nil {
where, args = append(where, "provider = "+placeholder(len(args)+1)), append(args, *delete.Provider)
}
if _, err := d.db.ExecContext(ctx, "DELETE FROM user_identity WHERE "+strings.Join(where, " AND "), args...); err != nil {
return err
}
return nil
}
...@@ -70,6 +70,22 @@ func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting) ...@@ -70,6 +70,22 @@ func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting)
return userSettingList, nil return userSettingList, nil
} }
func (d *DB) DeleteUserSettings(ctx context.Context, delete *store.DeleteUserSetting) error {
where, args := []string{"1 = 1"}, []any{}
if v := delete.Key; v != storepb.UserSetting_KEY_UNSPECIFIED {
where, args = append(where, "key = "+placeholder(len(args)+1)), append(args, v.String())
}
if v := delete.UserID; v != nil {
where, args = append(where, "user_id = "+placeholder(len(args)+1)), append(args, *v)
}
if _, err := d.db.ExecContext(ctx, "DELETE FROM user_setting WHERE "+strings.Join(where, " AND "), args...); err != nil {
return err
}
return nil
}
func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) { func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) {
// Simplified query: fetch all PERSONAL_ACCESS_TOKENS rows and search in Go // Simplified query: fetch all PERSONAL_ACCESS_TOKENS rows and search in Go
// This matches SQLite/MySQL behavior and avoids PostgreSQL's strict JSONB errors // This matches SQLite/MySQL behavior and avoids PostgreSQL's strict JSONB errors
......
package sqlite
import (
"context"
"strings"
"github.com/usememos/memos/store"
)
func (d *DB) CreateUserIdentity(ctx context.Context, create *store.UserIdentity) (*store.UserIdentity, error) {
stmt := "INSERT INTO `user_identity` (`user_id`, `provider`, `extern_uid`) VALUES (?, ?, ?) RETURNING `id`, `created_ts`, `updated_ts`"
if err := d.db.QueryRowContext(ctx, stmt, create.UserID, create.Provider, create.ExternUID).Scan(
&create.ID,
&create.CreatedTs,
&create.UpdatedTs,
); err != nil {
return nil, err
}
return create, nil
}
func (d *DB) ListUserIdentities(ctx context.Context, find *store.FindUserIdentity) ([]*store.UserIdentity, error) {
where, args := []string{"1 = 1"}, []any{}
if find.ID != nil {
where, args = append(where, "`id` = ?"), append(args, *find.ID)
}
if find.UserID != nil {
where, args = append(where, "`user_id` = ?"), append(args, *find.UserID)
}
if find.Provider != nil {
where, args = append(where, "`provider` = ?"), append(args, *find.Provider)
}
if find.ExternUID != nil {
where, args = append(where, "`extern_uid` = ?"), append(args, *find.ExternUID)
}
rows, err := d.db.QueryContext(ctx, `
SELECT
id,
user_id,
provider,
extern_uid,
created_ts,
updated_ts
FROM user_identity
WHERE `+strings.Join(where, " AND ")+`
ORDER BY id ASC`,
args...,
)
if err != nil {
return nil, err
}
defer rows.Close()
list := []*store.UserIdentity{}
for rows.Next() {
ui := &store.UserIdentity{}
if err := rows.Scan(
&ui.ID,
&ui.UserID,
&ui.Provider,
&ui.ExternUID,
&ui.CreatedTs,
&ui.UpdatedTs,
); err != nil {
return nil, err
}
list = append(list, ui)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (d *DB) DeleteUserIdentities(ctx context.Context, delete *store.DeleteUserIdentity) error {
where, args := []string{"1 = 1"}, []any{}
if delete.ID != nil {
where, args = append(where, "`id` = ?"), append(args, *delete.ID)
}
if delete.UserID != nil {
where, args = append(where, "`user_id` = ?"), append(args, *delete.UserID)
}
if delete.Provider != nil {
where, args = append(where, "`provider` = ?"), append(args, *delete.Provider)
}
if _, err := d.db.ExecContext(ctx, "DELETE FROM `user_identity` WHERE "+strings.Join(where, " AND "), args...); err != nil {
return err
}
return nil
}
...@@ -69,6 +69,22 @@ func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting) ...@@ -69,6 +69,22 @@ func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting)
return userSettingList, nil return userSettingList, nil
} }
func (d *DB) DeleteUserSettings(ctx context.Context, delete *store.DeleteUserSetting) error {
where, args := []string{"1 = 1"}, []any{}
if v := delete.Key; v != storepb.UserSetting_KEY_UNSPECIFIED {
where, args = append(where, "key = ?"), append(args, v.String())
}
if v := delete.UserID; v != nil {
where, args = append(where, "user_id = ?"), append(args, *v)
}
if _, err := d.db.ExecContext(ctx, "DELETE FROM user_setting WHERE "+strings.Join(where, " AND "), args...); err != nil {
return err
}
return nil
}
func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) { func (d *DB) GetUserByPATHash(ctx context.Context, tokenHash string) (*store.PATQueryResult, error) {
query := ` query := `
SELECT SELECT
......
...@@ -45,6 +45,7 @@ type Driver interface { ...@@ -45,6 +45,7 @@ type Driver interface {
// UserSetting model related methods. // UserSetting model related methods.
UpsertUserSetting(ctx context.Context, upsert *UserSetting) (*UserSetting, error) UpsertUserSetting(ctx context.Context, upsert *UserSetting) (*UserSetting, error)
ListUserSettings(ctx context.Context, find *FindUserSetting) ([]*UserSetting, error) ListUserSettings(ctx context.Context, find *FindUserSetting) ([]*UserSetting, error)
DeleteUserSettings(ctx context.Context, delete *DeleteUserSetting) error
GetUserByPATHash(ctx context.Context, tokenHash string) (*PATQueryResult, error) GetUserByPATHash(ctx context.Context, tokenHash string) (*PATQueryResult, error)
// IdentityProvider model related methods. // IdentityProvider model related methods.
...@@ -70,4 +71,9 @@ type Driver interface { ...@@ -70,4 +71,9 @@ type Driver interface {
ListMemoShares(ctx context.Context, find *FindMemoShare) ([]*MemoShare, error) ListMemoShares(ctx context.Context, find *FindMemoShare) ([]*MemoShare, error)
GetMemoShare(ctx context.Context, find *FindMemoShare) (*MemoShare, error) GetMemoShare(ctx context.Context, find *FindMemoShare) (*MemoShare, error)
DeleteMemoShare(ctx context.Context, delete *DeleteMemoShare) error DeleteMemoShare(ctx context.Context, delete *DeleteMemoShare) error
// UserIdentity model related methods.
CreateUserIdentity(ctx context.Context, create *UserIdentity) (*UserIdentity, error)
ListUserIdentities(ctx context.Context, find *FindUserIdentity) ([]*UserIdentity, error)
DeleteUserIdentities(ctx context.Context, delete *DeleteUserIdentity) error
} }
-- user_identity stores the linkage between an external identity subject and a local user.
-- (provider, extern_uid) is unique across the table; provider stores the idp.uid.
-- Each local user can link at most one external account per provider.
CREATE TABLE `user_identity` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL,
`provider` VARCHAR(256) NOT NULL,
`extern_uid` VARCHAR(256) NOT NULL,
`created_ts` BIGINT NOT NULL DEFAULT (UNIX_TIMESTAMP()),
`updated_ts` BIGINT NOT NULL DEFAULT (UNIX_TIMESTAMP()),
UNIQUE (`provider`, `extern_uid`),
UNIQUE (`user_id`, `provider`)
);
CREATE INDEX `idx_user_identity_user_id` ON `user_identity`(`user_id`);
...@@ -109,3 +109,17 @@ CREATE TABLE `memo_share` ( ...@@ -109,3 +109,17 @@ CREATE TABLE `memo_share` (
); );
CREATE INDEX `idx_memo_share_memo_id` ON `memo_share`(`memo_id`); CREATE INDEX `idx_memo_share_memo_id` ON `memo_share`(`memo_id`);
-- user_identity
CREATE TABLE `user_identity` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` INT NOT NULL,
`provider` VARCHAR(256) NOT NULL,
`extern_uid` VARCHAR(256) NOT NULL,
`created_ts` BIGINT NOT NULL DEFAULT (UNIX_TIMESTAMP()),
`updated_ts` BIGINT NOT NULL DEFAULT (UNIX_TIMESTAMP()),
UNIQUE (`provider`, `extern_uid`),
UNIQUE (`user_id`, `provider`)
);
CREATE INDEX `idx_user_identity_user_id` ON `user_identity`(`user_id`);
-- user_identity stores the linkage between an external identity subject and a local user.
-- (provider, extern_uid) is unique across the table; provider stores the idp.uid.
-- Each local user can link at most one external account per provider.
CREATE TABLE user_identity (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
provider TEXT NOT NULL,
extern_uid TEXT NOT NULL,
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
UNIQUE (provider, extern_uid),
UNIQUE (user_id, provider)
);
CREATE INDEX idx_user_identity_user_id ON user_identity(user_id);
...@@ -109,3 +109,17 @@ CREATE TABLE memo_share ( ...@@ -109,3 +109,17 @@ CREATE TABLE memo_share (
); );
CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id); CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id);
-- user_identity
CREATE TABLE user_identity (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
provider TEXT NOT NULL,
extern_uid TEXT NOT NULL,
created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
UNIQUE (provider, extern_uid),
UNIQUE (user_id, provider)
);
CREATE INDEX idx_user_identity_user_id ON user_identity(user_id);
-- user_identity stores the linkage between an external identity subject and a local user.
-- (provider, extern_uid) is unique across the table; provider stores the idp.uid.
-- Each local user can link at most one external account per provider.
CREATE TABLE user_identity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
provider TEXT NOT NULL,
extern_uid TEXT NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE (provider, extern_uid),
UNIQUE (user_id, provider)
);
CREATE INDEX idx_user_identity_user_id ON user_identity(user_id);
...@@ -110,3 +110,17 @@ CREATE TABLE memo_share ( ...@@ -110,3 +110,17 @@ CREATE TABLE memo_share (
); );
CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id); CREATE INDEX idx_memo_share_memo_id ON memo_share(memo_id);
-- user_identity
CREATE TABLE user_identity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
provider TEXT NOT NULL,
extern_uid TEXT NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE (provider, extern_uid),
UNIQUE (user_id, provider)
);
CREATE INDEX idx_user_identity_user_id ON user_identity(user_id);
package test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/usememos/memos/store"
)
func TestUserIdentityCreateAndGet(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
provider := "idp-uid-1"
externUID := "jane@example.com"
created, err := ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: provider,
ExternUID: externUID,
})
require.NoError(t, err)
require.NotZero(t, created.ID)
require.NotZero(t, created.CreatedTs)
require.Equal(t, user.ID, created.UserID)
require.Equal(t, provider, created.Provider)
require.Equal(t, externUID, created.ExternUID)
got, err := ts.GetUserIdentity(ctx, &store.FindUserIdentity{
Provider: &provider,
ExternUID: &externUID,
})
require.NoError(t, err)
require.NotNil(t, got)
require.Equal(t, created.ID, got.ID)
require.Equal(t, user.ID, got.UserID)
// Miss returns (nil, nil).
missingProvider := "idp-uid-missing"
notFound, err := ts.GetUserIdentity(ctx, &store.FindUserIdentity{
Provider: &missingProvider,
ExternUID: &externUID,
})
require.NoError(t, err)
require.Nil(t, notFound)
}
func TestUserIdentityListByUserID(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-A",
ExternUID: "sub-a-1",
})
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-B",
ExternUID: "sub-b-1",
})
require.NoError(t, err)
list, err := ts.ListUserIdentities(ctx, &store.FindUserIdentity{
UserID: &user.ID,
})
require.NoError(t, err)
require.Len(t, list, 2)
}
func TestUserIdentityUniqueConflict(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
userA, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
userB, err := createTestingUserWithRole(ctx, ts, "conflict_user", store.RoleUser)
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: userA.ID,
Provider: "idp-A",
ExternUID: "sub-1",
})
require.NoError(t, err)
// Second insert with the same (provider, extern_uid) must fail regardless of user_id.
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: userB.ID,
Provider: "idp-A",
ExternUID: "sub-1",
})
require.Error(t, err)
}
func TestUserIdentitySameExternUIDDifferentProviders(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-A",
ExternUID: "sub-1",
})
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-B",
ExternUID: "sub-1",
})
require.NoError(t, err)
externUID := "sub-1"
list, err := ts.ListUserIdentities(ctx, &store.FindUserIdentity{
ExternUID: &externUID,
})
require.NoError(t, err)
require.Len(t, list, 2)
}
func TestUserIdentitySameUserSameProviderConflicts(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-A",
ExternUID: "sub-1",
})
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-A",
ExternUID: "sub-2",
})
require.Error(t, err)
}
func TestUserIdentityDeleteByUserAndProvider(t *testing.T) {
t.Parallel()
ctx := context.Background()
ts := NewTestingStore(ctx, t)
defer ts.Close()
user, err := createTestingHostUser(ctx, ts)
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-A",
ExternUID: "sub-a-1",
})
require.NoError(t, err)
_, err = ts.CreateUserIdentity(ctx, &store.UserIdentity{
UserID: user.ID,
Provider: "idp-B",
ExternUID: "sub-b-1",
})
require.NoError(t, err)
provider := "idp-A"
err = ts.DeleteUserIdentities(ctx, &store.DeleteUserIdentity{
UserID: &user.ID,
Provider: &provider,
})
require.NoError(t, err)
list, err := ts.ListUserIdentities(ctx, &store.FindUserIdentity{
UserID: &user.ID,
})
require.NoError(t, err)
require.Len(t, list, 1)
require.Equal(t, "idp-B", list[0].Provider)
}
package store
import "context"
// UserIdentity is the linkage between an external identity subject and a local user.
// Uniqueness is enforced on (Provider, ExternUID); one local user may have multiple
// identities across different providers.
type UserIdentity struct {
ID int32
UserID int32
Provider string
ExternUID string
CreatedTs int64
UpdatedTs int64
}
// FindUserIdentity is used to filter user identities in list/get queries.
type FindUserIdentity struct {
ID *int32
UserID *int32
Provider *string
ExternUID *string
}
// DeleteUserIdentity is used to delete user identity linkage rows.
type DeleteUserIdentity struct {
ID *int32
UserID *int32
Provider *string
}
// CreateUserIdentity creates a new external-identity linkage record.
// Returns the driver error on unique-constraint violation; callers are responsible
// for reconciling concurrent first-login races on (Provider, ExternUID).
func (s *Store) CreateUserIdentity(ctx context.Context, create *UserIdentity) (*UserIdentity, error) {
return s.driver.CreateUserIdentity(ctx, create)
}
// ListUserIdentities returns all linkage records matching the filter.
func (s *Store) ListUserIdentities(ctx context.Context, find *FindUserIdentity) ([]*UserIdentity, error) {
return s.driver.ListUserIdentities(ctx, find)
}
// DeleteUserIdentities deletes all linkage records matching the filter.
func (s *Store) DeleteUserIdentities(ctx context.Context, delete *DeleteUserIdentity) error {
return s.driver.DeleteUserIdentities(ctx, delete)
}
// GetUserIdentity returns the first linkage record matching the filter, or nil if none found.
func (s *Store) GetUserIdentity(ctx context.Context, find *FindUserIdentity) (*UserIdentity, error) {
list, err := s.ListUserIdentities(ctx, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, nil
}
return list[0], nil
}
...@@ -21,6 +21,11 @@ type FindUserSetting struct { ...@@ -21,6 +21,11 @@ type FindUserSetting struct {
Key storepb.UserSetting_Key Key storepb.UserSetting_Key
} }
type DeleteUserSetting struct {
UserID *int32
Key storepb.UserSetting_Key
}
// RefreshTokenQueryResult contains the result of querying a refresh token. // RefreshTokenQueryResult contains the result of querying a refresh token.
type RefreshTokenQueryResult struct { type RefreshTokenQueryResult struct {
UserID int32 UserID int32
...@@ -102,6 +107,23 @@ func (s *Store) GetUserSetting(ctx context.Context, find *FindUserSetting) (*sto ...@@ -102,6 +107,23 @@ func (s *Store) GetUserSetting(ctx context.Context, find *FindUserSetting) (*sto
return userSetting, nil return userSetting, nil
} }
func (s *Store) DeleteUserSettings(ctx context.Context, delete *DeleteUserSetting) error {
existing, err := s.ListUserSettings(ctx, &FindUserSetting{
UserID: delete.UserID,
Key: delete.Key,
})
if err != nil {
return err
}
if err := s.driver.DeleteUserSettings(ctx, delete); err != nil {
return err
}
for _, setting := range existing {
s.userSettingCache.Delete(ctx, getUserSettingCacheKey(setting.UserId, setting.Key.String()))
}
return nil
}
// GetUserByPATHash finds a user by PAT hash. // GetUserByPATHash finds a user by PAT hash.
func (s *Store) GetUserByPATHash(ctx context.Context, tokenHash string) (*PATQueryResult, error) { func (s *Store) GetUserByPATHash(ctx context.Context, tokenHash string) (*PATQueryResult, error) {
result, err := s.driver.GetUserByPATHash(ctx, tokenHash) result, err := s.driver.GetUserByPATHash(ctx, tokenHash)
......
import { useEffect, useMemo, useState } from "react";
import { toast } from "react-hot-toast";
import { Button } from "@/components/ui/button";
import { identityProviderServiceClient, userServiceClient } from "@/connect";
import { absolutifyLink } from "@/helpers/utils";
import useCurrentUser from "@/hooks/useCurrentUser";
import { handleError } from "@/lib/error";
import { IdentityProvider, IdentityProvider_Type } from "@/types/proto/api/v1/idp_service_pb";
import { LinkedIdentity } from "@/types/proto/api/v1/user_service_pb";
import { useTranslate } from "@/utils/i18n";
import { storeOAuthState } from "@/utils/oauth";
import SettingGroup from "./SettingGroup";
import SettingTable from "./SettingTable";
interface LinkedIdentityRow extends Record<string, unknown> {
name: string;
title: string;
externUid: string;
linkedIdentity?: LinkedIdentity;
identityProvider: IdentityProvider;
}
const LinkedIdentitySection = () => {
const t = useTranslate();
const currentUser = useCurrentUser();
const [identityProviderList, setIdentityProviderList] = useState<IdentityProvider[]>([]);
const [linkedIdentityList, setLinkedIdentityList] = useState<LinkedIdentity[]>([]);
const fetchData = async () => {
if (!currentUser?.name) {
return;
}
const [{ identityProviders }, { linkedIdentities }] = await Promise.all([
identityProviderServiceClient.listIdentityProviders({}),
userServiceClient.listLinkedIdentities({ parent: currentUser.name }),
]);
setIdentityProviderList(identityProviders);
setLinkedIdentityList(linkedIdentities);
};
useEffect(() => {
if (!currentUser?.name) {
return;
}
fetchData().catch((error: unknown) => {
handleError(error, toast.error, {
context: "Load linked identities",
});
});
}, [currentUser?.name]);
const oauthIdentityProviders = useMemo(
() => identityProviderList.filter((identityProvider) => identityProvider.type === IdentityProvider_Type.OAUTH2),
[identityProviderList],
);
const linkedIdentityByProviderName = useMemo(() => {
const mapping = new Map<string, LinkedIdentity>();
for (const linkedIdentity of linkedIdentityList) {
if (!mapping.has(linkedIdentity.idpName)) {
mapping.set(linkedIdentity.idpName, linkedIdentity);
}
}
return mapping;
}, [linkedIdentityList]);
const rows = useMemo<LinkedIdentityRow[]>(
() =>
oauthIdentityProviders.map((identityProvider) => {
const linkedIdentity = linkedIdentityByProviderName.get(identityProvider.name);
return {
name: identityProvider.name,
title: identityProvider.title,
externUid: linkedIdentity?.externUid ?? "",
linkedIdentity,
identityProvider,
};
}),
[linkedIdentityByProviderName, oauthIdentityProviders],
);
const handleLinkIdentityProvider = async (identityProvider: IdentityProvider) => {
if (!currentUser?.name) {
return;
}
const redirectUri = absolutifyLink("/auth/callback");
const oauth2Config = identityProvider.config?.config?.case === "oauth2Config" ? identityProvider.config.config.value : undefined;
if (!oauth2Config) {
toast.error("Identity provider configuration is invalid.");
return;
}
try {
const returnUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`;
const { state, codeChallenge } = await storeOAuthState(identityProvider.name, "link", returnUrl, currentUser.name);
let authUrl = `${oauth2Config.authUrl}?client_id=${
oauth2Config.clientId
}&redirect_uri=${encodeURIComponent(redirectUri)}&state=${state}&response_type=code&scope=${encodeURIComponent(
oauth2Config.scopes.join(" "),
)}`;
if (codeChallenge) {
authUrl += `&code_challenge=${codeChallenge}&code_challenge_method=S256`;
}
window.location.href = authUrl;
} catch (error) {
handleError(error, toast.error, {
context: "Failed to initiate OAuth flow",
fallbackMessage: "Failed to initiate account linking. Please try again.",
});
}
};
const handleUnlinkIdentityProvider = async (row: LinkedIdentityRow) => {
if (!row.linkedIdentity?.name) {
return;
}
try {
await userServiceClient.deleteLinkedIdentity({
name: row.linkedIdentity.name,
});
await fetchData();
toast.success(`Unlinked ${row.title}.`);
} catch (error) {
handleError(error, toast.error, {
context: "Delete linked identity",
fallbackMessage: "Failed to unlink identity provider.",
});
}
};
if (oauthIdentityProviders.length === 0) {
return null;
}
return (
<SettingGroup
showSeparator
title="SSO accounts"
description="Each provider can be linked to this account at most once. A linked row shows the current extern_uid and can be unlinked."
>
<SettingTable<LinkedIdentityRow>
columns={[
{
key: "title",
header: "SSO provider",
render: (_, row: LinkedIdentityRow) => <span className="text-foreground">{row.title}</span>,
},
{
key: "externUid",
header: "extern_uid",
render: (_, row: LinkedIdentityRow) => (
<span className={row.externUid ? "text-foreground" : "text-muted-foreground"}>
{row.externUid || t("attachment-library.labels.not-linked")}
</span>
),
},
{
key: "actions",
header: "",
className: "text-right",
render: (_, row: LinkedIdentityRow) =>
row.linkedIdentity ? (
<Button variant="outline" size="sm" onClick={() => handleUnlinkIdentityProvider(row)}>
Unlink
</Button>
) : (
<Button variant="outline" size="sm" onClick={() => handleLinkIdentityProvider(row.identityProvider)}>
{t("common.link")}
</Button>
),
},
]}
data={rows}
emptyMessage="No SSO providers found."
getRowKey={(row) => row.name}
/>
</SettingGroup>
);
};
export default LinkedIdentitySection;
import { MoreVerticalIcon, PenLineIcon } from "lucide-react"; import { MoreVerticalIcon, PenLineIcon } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import ConfirmDialog from "@/components/ConfirmDialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { userServiceClient } from "@/connect";
import { useAuth } from "@/contexts/AuthContext";
import useCurrentUser from "@/hooks/useCurrentUser"; import useCurrentUser from "@/hooks/useCurrentUser";
import { useDialog } from "@/hooks/useDialog"; import { useDialog } from "@/hooks/useDialog";
import useNavigateTo from "@/hooks/useNavigateTo";
import { handleError } from "@/lib/error";
import { ROUTES } from "@/router/routes";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import ChangeMemberPasswordDialog from "../ChangeMemberPasswordDialog"; import ChangeMemberPasswordDialog from "../ChangeMemberPasswordDialog";
import UpdateAccountDialog from "../UpdateAccountDialog"; import UpdateAccountDialog from "../UpdateAccountDialog";
import UserAvatar from "../UserAvatar"; import UserAvatar from "../UserAvatar";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/dropdown-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/dropdown-menu";
import AccessTokenSection from "./AccessTokenSection"; import AccessTokenSection from "./AccessTokenSection";
import LinkedIdentitySection from "./LinkedIdentitySection";
import SettingGroup from "./SettingGroup"; import SettingGroup from "./SettingGroup";
import SettingSection from "./SettingSection"; import SettingSection from "./SettingSection";
const MyAccountSection = () => { const MyAccountSection = () => {
const t = useTranslate(); const t = useTranslate();
const user = useCurrentUser(); const user = useCurrentUser();
const { logout } = useAuth();
const navigateTo = useNavigateTo();
const accountDialog = useDialog(); const accountDialog = useDialog();
const passwordDialog = useDialog(); const passwordDialog = useDialog();
const [deleteOpen, setDeleteOpen] = useState(false);
const handleDeleteAccount = async () => {
if (!user?.name) {
return;
}
try {
await userServiceClient.deleteUser({ name: user.name });
await logout();
toast.success(t("setting.member.delete-success", { username: user.username }));
navigateTo(ROUTES.AUTH, { replace: true });
} catch (error) {
handleError(error, toast.error, { context: "Delete account" });
throw error;
}
};
return ( return (
<SettingSection title={t("setting.my-account.label")}> <SettingSection title={t("setting.my-account.label")}>
...@@ -42,21 +69,35 @@ const MyAccountSection = () => { ...@@ -42,21 +69,35 @@ const MyAccountSection = () => {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={passwordDialog.open}>{t("setting.account.change-password")}</DropdownMenuItem> <DropdownMenuItem onClick={passwordDialog.open}>{t("setting.account.change-password")}</DropdownMenuItem>
<DropdownMenuItem onClick={() => setDeleteOpen(true)} className="text-destructive focus:text-destructive">
{t("setting.account.delete-account")}
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
</div> </div>
</SettingGroup> </SettingGroup>
<SettingGroup showSeparator> <LinkedIdentitySection />
<AccessTokenSection />
</SettingGroup> <AccessTokenSection />
{/* Update Account Dialog */} {/* Update Account Dialog */}
<UpdateAccountDialog open={accountDialog.isOpen} onOpenChange={accountDialog.setOpen} /> <UpdateAccountDialog open={accountDialog.isOpen} onOpenChange={accountDialog.setOpen} />
{/* Change Password Dialog */} {/* Change Password Dialog */}
<ChangeMemberPasswordDialog open={passwordDialog.isOpen} onOpenChange={passwordDialog.setOpen} user={user} /> <ChangeMemberPasswordDialog open={passwordDialog.isOpen} onOpenChange={passwordDialog.setOpen} user={user} />
<ConfirmDialog
open={deleteOpen}
onOpenChange={setDeleteOpen}
title={user ? t("setting.member.delete-warning", { username: user.username }) : ""}
description={t("setting.member.delete-warning-description")}
confirmLabel={t("common.delete")}
cancelLabel={t("common.cancel")}
onConfirm={handleDeleteAccount}
confirmVariant="destructive"
/>
</SettingSection> </SettingSection>
); );
}; };
......
import { MoreVerticalIcon, PlusIcon } from "lucide-react"; import { MoreVerticalIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import ConfirmDialog from "@/components/ConfirmDialog"; import ConfirmDialog from "@/components/ConfirmDialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
...@@ -7,13 +7,23 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge ...@@ -7,13 +7,23 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
import { identityProviderServiceClient } from "@/connect"; import { identityProviderServiceClient } from "@/connect";
import { useDialog } from "@/hooks/useDialog"; import { useDialog } from "@/hooks/useDialog";
import { handleError } from "@/lib/error"; import { handleError } from "@/lib/error";
import { IdentityProvider } from "@/types/proto/api/v1/idp_service_pb"; import { IdentityProvider, IdentityProvider_Type } from "@/types/proto/api/v1/idp_service_pb";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import CreateIdentityProviderDialog from "../CreateIdentityProviderDialog"; import CreateIdentityProviderDialog from "../CreateIdentityProviderDialog";
import LearnMore from "../LearnMore"; import LearnMore from "../LearnMore";
import SettingSection from "./SettingSection"; import SettingSection from "./SettingSection";
import SettingTable from "./SettingTable"; import SettingTable from "./SettingTable";
interface IdentityProviderRow extends Record<string, unknown> {
name: string;
providerUid: string;
title: string;
typeLabel: string;
provider: IdentityProvider;
}
const getIdentityProviderUID = (name: string) => name.replace(/^identity-providers\//, "");
const SSOSection = () => { const SSOSection = () => {
const t = useTranslate(); const t = useTranslate();
const [identityProviderList, setIdentityProviderList] = useState<IdentityProvider[]>([]); const [identityProviderList, setIdentityProviderList] = useState<IdentityProvider[]>([]);
...@@ -22,14 +32,32 @@ const SSOSection = () => { ...@@ -22,14 +32,32 @@ const SSOSection = () => {
const idpDialog = useDialog(); const idpDialog = useDialog();
const fetchIdentityProviderList = async () => { const fetchIdentityProviderList = async () => {
const { identityProviders } = await identityProviderServiceClient.listIdentityProviders({}); try {
setIdentityProviderList(identityProviders); const { identityProviders } = await identityProviderServiceClient.listIdentityProviders({});
setIdentityProviderList(identityProviders);
} catch (error: unknown) {
handleError(error, toast.error, {
context: "Load identity providers",
});
}
}; };
useEffect(() => { useEffect(() => {
fetchIdentityProviderList(); void fetchIdentityProviderList();
}, []); }, []);
const rows = useMemo<IdentityProviderRow[]>(
() =>
identityProviderList.map((provider) => ({
name: provider.name,
providerUid: getIdentityProviderUID(provider.name),
title: provider.title,
typeLabel: IdentityProvider_Type[provider.type] ?? "TYPE_UNSPECIFIED",
provider,
})),
[identityProviderList],
);
const handleDeleteIdentityProvider = (identityProvider: IdentityProvider) => { const handleDeleteIdentityProvider = (identityProvider: IdentityProvider) => {
setDeleteTarget(identityProvider); setDeleteTarget(identityProvider);
}; };
...@@ -88,20 +116,25 @@ const SSOSection = () => { ...@@ -88,20 +116,25 @@ const SSOSection = () => {
<SettingTable <SettingTable
columns={[ columns={[
{ {
key: "title", key: "providerUid",
header: t("common.name"), header: "provider_uid",
render: (_, provider: IdentityProvider) => ( render: (_, row: IdentityProviderRow) => (
<span className="text-foreground"> <div className="flex flex-col">
{provider.title} <span className="text-foreground">{row.providerUid}</span>
<span className="ml-2 text-sm text-muted-foreground">({provider.type})</span> {row.title ? <span className="text-sm text-muted-foreground">{row.title}</span> : null}
</span> </div>
), ),
}, },
{
key: "typeLabel",
header: t("common.type"),
render: (_, row: IdentityProviderRow) => <span className="text-muted-foreground">{row.typeLabel}</span>,
},
{ {
key: "actions", key: "actions",
header: "", header: "",
className: "text-right", className: "text-right",
render: (_, provider: IdentityProvider) => ( render: (_, row: IdentityProviderRow) => (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" size="sm"> <Button variant="outline" size="sm">
...@@ -109,9 +142,9 @@ const SSOSection = () => { ...@@ -109,9 +142,9 @@ const SSOSection = () => {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={2}> <DropdownMenuContent align="end" sideOffset={2}>
<DropdownMenuItem onClick={() => handleEditIdentityProvider(provider)}>{t("common.edit")}</DropdownMenuItem> <DropdownMenuItem onClick={() => handleEditIdentityProvider(row.provider)}>{t("common.edit")}</DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => handleDeleteIdentityProvider(provider)} onClick={() => handleDeleteIdentityProvider(row.provider)}
className="text-destructive focus:text-destructive" className="text-destructive focus:text-destructive"
> >
{t("common.delete")} {t("common.delete")}
...@@ -121,9 +154,9 @@ const SSOSection = () => { ...@@ -121,9 +154,9 @@ const SSOSection = () => {
), ),
}, },
]} ]}
data={identityProviderList} data={rows}
emptyMessage={t("setting.sso.no-sso-found")} emptyMessage={t("setting.sso.no-sso-found")}
getRowKey={(provider) => provider.name} getRowKey={(row) => row.name}
/> />
<CreateIdentityProviderDialog <CreateIdentityProviderDialog
......
...@@ -386,6 +386,7 @@ ...@@ -386,6 +386,7 @@
}, },
"account": { "account": {
"change-password": "Change password", "change-password": "Change password",
"delete-account": "Delete account",
"email-note": "Optional", "email-note": "Optional",
"export-memos": "Export Memos", "export-memos": "Export Memos",
"nickname-note": "Displayed in the banner", "nickname-note": "Displayed in the banner",
......
...@@ -493,6 +493,7 @@ ...@@ -493,6 +493,7 @@
}, },
"account": { "account": {
"change-password": "修改密码", "change-password": "修改密码",
"delete-account": "删除账号",
"email-note": "可选", "email-note": "可选",
"export-memos": "导出备忘录", "export-memos": "导出备忘录",
"nickname-note": "显示在横幅中", "nickname-note": "显示在横幅中",
......
...@@ -2,7 +2,7 @@ import { timestampDate } from "@bufbuild/protobuf/wkt"; ...@@ -2,7 +2,7 @@ import { timestampDate } from "@bufbuild/protobuf/wkt";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useSearchParams } from "react-router-dom"; import { useSearchParams } from "react-router-dom";
import { setAccessToken } from "@/auth-state"; import { setAccessToken } from "@/auth-state";
import { authServiceClient } from "@/connect"; import { authServiceClient, userServiceClient } from "@/connect";
import { useAuth } from "@/contexts/AuthContext"; import { useAuth } from "@/contexts/AuthContext";
import { absolutifyLink } from "@/helpers/utils"; import { absolutifyLink } from "@/helpers/utils";
import useNavigateTo from "@/hooks/useNavigateTo"; import useNavigateTo from "@/hooks/useNavigateTo";
...@@ -18,7 +18,7 @@ interface State { ...@@ -18,7 +18,7 @@ interface State {
const AuthCallback = () => { const AuthCallback = () => {
const navigateTo = useNavigateTo(); const navigateTo = useNavigateTo();
const { initialize } = useAuth(); const { currentUser, initialize, isInitialized } = useAuth();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const handledRef = useRef(false); const handledRef = useRef(false);
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
...@@ -27,10 +27,12 @@ const AuthCallback = () => { ...@@ -27,10 +27,12 @@ const AuthCallback = () => {
}); });
useEffect(() => { useEffect(() => {
if (!isInitialized) {
return;
}
if (handledRef.current) { if (handledRef.current) {
return; return;
} }
handledRef.current = true;
// Check for OAuth error response first (e.g., user denied access) // Check for OAuth error response first (e.g., user denied access)
const error = searchParams.get("error"); const error = searchParams.get("error");
const errorDescription = searchParams.get("error_description"); const errorDescription = searchParams.get("error_description");
...@@ -74,25 +76,42 @@ const AuthCallback = () => { ...@@ -74,25 +76,42 @@ const AuthCallback = () => {
return; return;
} }
const { identityProviderName, returnUrl, codeVerifier } = validatedState; const { flowMode, identityProviderName, returnUrl, linkingUserName, codeVerifier } = validatedState;
const redirectUri = absolutifyLink("/auth/callback"); const redirectUri = absolutifyLink("/auth/callback");
handledRef.current = true;
(async () => { (async () => {
try { try {
const response = await authServiceClient.signIn({ if (flowMode === "link") {
credentials: { if (!currentUser?.name) {
case: "ssoCredentials", throw new Error("Failed to link account. Please sign in to Memos again and retry.");
value: { }
idpName: identityProviderName, if (linkingUserName && currentUser.name !== linkingUserName) {
code, throw new Error("The signed-in user changed before the OAuth callback completed. Please retry linking from account settings.");
redirectUri, }
codeVerifier: codeVerifier || "", // Pass PKCE code_verifier for token exchange await userServiceClient.createLinkedIdentity({
parent: currentUser.name,
idpName: identityProviderName,
code,
redirectUri,
codeVerifier: codeVerifier || "",
});
} else {
const response = await authServiceClient.signIn({
credentials: {
case: "ssoCredentials",
value: {
idpName: identityProviderName,
code,
redirectUri,
codeVerifier: codeVerifier || "", // Pass PKCE code_verifier for token exchange
},
}, },
}, });
}); // Store access token from login response
// Store access token from login response if (response.accessToken) {
if (response.accessToken) { setAccessToken(response.accessToken, response.accessTokenExpiresAt ? timestampDate(response.accessTokenExpiresAt) : undefined);
setAccessToken(response.accessToken, response.accessTokenExpiresAt ? timestampDate(response.accessTokenExpiresAt) : undefined); }
} }
setState({ setState({
loading: false, loading: false,
...@@ -116,7 +135,7 @@ const AuthCallback = () => { ...@@ -116,7 +135,7 @@ const AuthCallback = () => {
}); });
} }
})(); })();
}, [searchParams, navigateTo]); }, [currentUser?.name, initialize, isInitialized, navigateTo, searchParams]);
if (state.loading) return null; if (state.loading) return null;
......
...@@ -44,7 +44,7 @@ const SignIn = () => { ...@@ -44,7 +44,7 @@ const SignIn = () => {
try { try {
// Generate and store secure state parameter with CSRF protection // Generate and store secure state parameter with CSRF protection
// Also generate PKCE parameters (code_challenge) for enhanced security if available // Also generate PKCE parameters (code_challenge) for enhanced security if available
const { state, codeChallenge } = await storeOAuthState(identityProvider.name, redirectTarget); const { state, codeChallenge } = await storeOAuthState(identityProvider.name, "signin", redirectTarget);
// Build OAuth authorization URL with secure state // Build OAuth authorization URL with secure state
// Include PKCE if available (requires HTTPS/localhost for crypto.subtle) // Include PKCE if available (requires HTTPS/localhost for crypto.subtle)
......
...@@ -18,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf"; ...@@ -18,7 +18,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file api/v1/user_service.proto. * Describes the file api/v1/user_service.proto.
*/ */
export const file_api_v1_user_service: GenFile = /*@__PURE__*/ export const file_api_v1_user_service: GenFile = /*@__PURE__*/
fileDesc("ChlhcGkvdjEvdXNlcl9zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEi1gMKBFVzZXISEQoEbmFtZRgBIAEoCUID4EEIEioKBHJvbGUYAiABKA4yFy5tZW1vcy5hcGkudjEuVXNlci5Sb2xlQgPgQQISFQoIdXNlcm5hbWUYAyABKAlCA+BBAhISCgVlbWFpbBgEIAEoCUID4EEBEhkKDGRpc3BsYXlfbmFtZRgFIAEoCUID4EEBEhcKCmF2YXRhcl91cmwYBiABKAlCA+BBARIYCgtkZXNjcmlwdGlvbhgHIAEoCUID4EEBEhUKCHBhc3N3b3JkGAggASgJQgPgQQQSJwoFc3RhdGUYCSABKA4yEy5tZW1vcy5hcGkudjEuU3RhdGVCA+BBAhI0CgtjcmVhdGVfdGltZRgKIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxI0Cgt1cGRhdGVfdGltZRgLIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAyIxCgRSb2xlEhQKEFJPTEVfVU5TUEVDSUZJRUQQABIJCgVBRE1JThACEggKBFVTRVIQAzo36kE0ChFtZW1vcy5hcGkudjEvVXNlchIMdXNlcnMve3VzZXJ9GgRuYW1lKgV1c2VyczIEdXNlciJzChBMaXN0VXNlcnNSZXF1ZXN0EhYKCXBhZ2Vfc2l6ZRgBIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAiABKAlCA+BBARITCgZmaWx0ZXIYAyABKAlCA+BBARIZCgxzaG93X2RlbGV0ZWQYBCABKAhCA+BBASJjChFMaXN0VXNlcnNSZXNwb25zZRIhCgV1c2VycxgBIAMoCzISLm1lbW9zLmFwaS52MS5Vc2VyEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRISCgp0b3RhbF9zaXplGAMgASgFIikKFEJhdGNoR2V0VXNlcnNSZXF1ZXN0EhEKCXVzZXJuYW1lcxgBIAMoCSI6ChVCYXRjaEdldFVzZXJzUmVzcG9uc2USIQoFdXNlcnMYASADKAsyEi5tZW1vcy5hcGkudjEuVXNlciJtCg5HZXRVc2VyUmVxdWVzdBInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEjIKCXJlYWRfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBASKIAQoRQ3JlYXRlVXNlclJlcXVlc3QSKAoEdXNlchgBIAEoCzISLm1lbW9zLmFwaS52MS5Vc2VyQgbgQQLgQQQSFAoHdXNlcl9pZBgCIAEoCUID4EEBEhoKDXZhbGlkYXRlX29ubHkYAyABKAhCA+BBARIXCgpyZXF1ZXN0X2lkGAQgASgJQgPgQQEijAEKEVVwZGF0ZVVzZXJSZXF1ZXN0EiUKBHVzZXIYASABKAsyEi5tZW1vcy5hcGkudjEuVXNlckID4EECEjQKC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EECEhoKDWFsbG93X21pc3NpbmcYAyABKAhCA+BBASJQChFEZWxldGVVc2VyUmVxdWVzdBInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhIKBWZvcmNlGAIgASgIQgPgQQEi2AMKCVVzZXJTdGF0cxIRCgRuYW1lGAEgASgJQgPgQQgSOwoXbWVtb19kaXNwbGF5X3RpbWVzdGFtcHMYAiADKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEj4KD21lbW9fdHlwZV9zdGF0cxgDIAEoCzIlLm1lbW9zLmFwaS52MS5Vc2VyU3RhdHMuTWVtb1R5cGVTdGF0cxI4Cgl0YWdfY291bnQYBCADKAsyJS5tZW1vcy5hcGkudjEuVXNlclN0YXRzLlRhZ0NvdW50RW50cnkSFAoMcGlubmVkX21lbW9zGAUgAygJEhgKEHRvdGFsX21lbW9fY291bnQYBiABKAUaLwoNVGFnQ291bnRFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAU6AjgBGl8KDU1lbW9UeXBlU3RhdHMSEgoKbGlua19jb3VudBgBIAEoBRISCgpjb2RlX2NvdW50GAIgASgFEhIKCnRvZG9fY291bnQYAyABKAUSEgoKdW5kb19jb3VudBgEIAEoBTo/6kE8ChZtZW1vcy5hcGkudjEvVXNlclN0YXRzEgx1c2Vycy97dXNlcn0qCXVzZXJTdGF0czIJdXNlclN0YXRzIj4KE0dldFVzZXJTdGF0c1JlcXVlc3QSJwoEbmFtZRgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlciIZChdMaXN0QWxsVXNlclN0YXRzUmVxdWVzdCJCChhMaXN0QWxsVXNlclN0YXRzUmVzcG9uc2USJgoFc3RhdHMYASADKAsyFy5tZW1vcy5hcGkudjEuVXNlclN0YXRzIuQDCgtVc2VyU2V0dGluZxIRCgRuYW1lGAEgASgJQgPgQQgSQwoPZ2VuZXJhbF9zZXR0aW5nGAIgASgLMigubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nLkdlbmVyYWxTZXR0aW5nSAASRQoQd2ViaG9va3Nfc2V0dGluZxgFIAEoCzIpLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZy5XZWJob29rc1NldHRpbmdIABpXCg5HZW5lcmFsU2V0dGluZxITCgZsb2NhbGUYASABKAlCA+BBARIcCg9tZW1vX3Zpc2liaWxpdHkYAyABKAlCA+BBARISCgV0aGVtZRgEIAEoCUID4EEBGj4KD1dlYmhvb2tzU2V0dGluZxIrCgh3ZWJob29rcxgBIAMoCzIZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9vayI1CgNLZXkSEwoPS0VZX1VOU1BFQ0lGSUVEEAASCwoHR0VORVJBTBABEgwKCFdFQkhPT0tTEAQ6XepBWgoYbWVtb3MuYXBpLnYxL1VzZXJTZXR0aW5nEiN1c2Vycy97dXNlcm5hbWV9L3NldHRpbmdzL3tzZXR0aW5nfSoMdXNlclNldHRpbmdzMgt1c2VyU2V0dGluZ0IHCgV2YWx1ZSJHChVHZXRVc2VyU2V0dGluZ1JlcXVlc3QSLgoEbmFtZRgBIAEoCUIg4EEC+kEaChhtZW1vcy5hcGkudjEvVXNlclNldHRpbmcigQEKGFVwZGF0ZVVzZXJTZXR0aW5nUmVxdWVzdBIvCgdzZXR0aW5nGAEgASgLMhkubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nQgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQIidQoXTGlzdFVzZXJTZXR0aW5nc1JlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhYKCXBhZ2Vfc2l6ZRgCIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAyABKAlCA+BBASJ0ChhMaXN0VXNlclNldHRpbmdzUmVzcG9uc2USKwoIc2V0dGluZ3MYASADKAsyGS5tZW1vcy5hcGkudjEuVXNlclNldHRpbmcSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhIKCnRvdGFsX3NpemUYAyABKAUi8gIKE1BlcnNvbmFsQWNjZXNzVG9rZW4SEQoEbmFtZRgBIAEoCUID4EEIEhgKC2Rlc2NyaXB0aW9uGAIgASgJQgPgQQESMwoKY3JlYXRlZF9hdBgDIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxIzCgpleHBpcmVzX2F0GAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEBEjUKDGxhc3RfdXNlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAzqMAepBiAEKIG1lbW9zLmFwaS52MS9QZXJzb25hbEFjY2Vzc1Rva2VuEjl1c2Vycy97dXNlcn0vcGVyc29uYWxBY2Nlc3NUb2tlbnMve3BlcnNvbmFsX2FjY2Vzc190b2tlbn0qFHBlcnNvbmFsQWNjZXNzVG9rZW5zMhNwZXJzb25hbEFjY2Vzc1Rva2VuIn0KH0xpc3RQZXJzb25hbEFjY2Vzc1Rva2Vuc1JlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhYKCXBhZ2Vfc2l6ZRgCIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAyABKAlCA+BBASKSAQogTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zUmVzcG9uc2USQQoWcGVyc29uYWxfYWNjZXNzX3Rva2VucxgBIAMoCzIhLm1lbW9zLmFwaS52MS5QZXJzb25hbEFjY2Vzc1Rva2VuEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRISCgp0b3RhbF9zaXplGAMgASgFIoUBCiBDcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISGAoLZGVzY3JpcHRpb24YAiABKAlCA+BBARIcCg9leHBpcmVzX2luX2RheXMYAyABKAVCA+BBASJ0CiFDcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVzcG9uc2USQAoVcGVyc29uYWxfYWNjZXNzX3Rva2VuGAEgASgLMiEubWVtb3MuYXBpLnYxLlBlcnNvbmFsQWNjZXNzVG9rZW4SDQoFdG9rZW4YAiABKAkiWgogRGVsZXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlcXVlc3QSNgoEbmFtZRgBIAEoCUIo4EEC+kEiCiBtZW1vcy5hcGkudjEvUGVyc29uYWxBY2Nlc3NUb2tlbiKqAQoLVXNlcldlYmhvb2sSDAoEbmFtZRgBIAEoCRILCgN1cmwYAiABKAkSFAoMZGlzcGxheV9uYW1lGAMgASgJEjQKC2NyZWF0ZV90aW1lGAQgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDEjQKC3VwZGF0ZV90aW1lGAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDIi4KF0xpc3RVc2VyV2ViaG9va3NSZXF1ZXN0EhMKBnBhcmVudBgBIAEoCUID4EECIkcKGExpc3RVc2VyV2ViaG9va3NSZXNwb25zZRIrCgh3ZWJob29rcxgBIAMoCzIZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9vayJgChhDcmVhdGVVc2VyV2ViaG9va1JlcXVlc3QSEwoGcGFyZW50GAEgASgJQgPgQQISLwoHd2ViaG9vaxgCIAEoCzIZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9va0ID4EECInwKGFVwZGF0ZVVzZXJXZWJob29rUmVxdWVzdBIvCgd3ZWJob29rGAEgASgLMhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rQgPgQQISLwoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrIi0KGERlbGV0ZVVzZXJXZWJob29rUmVxdWVzdBIRCgRuYW1lGAEgASgJQgPgQQIiogcKEFVzZXJOb3RpZmljYXRpb24SFAoEbmFtZRgBIAEoCUIG4EED4EEIEikKBnNlbmRlchgCIAEoCUIZ4EED+kETChFtZW1vcy5hcGkudjEvVXNlchIsCgtzZW5kZXJfdXNlchgIIAEoCzISLm1lbW9zLmFwaS52MS5Vc2VyQgPgQQMSOgoGc3RhdHVzGAMgASgOMiUubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24uU3RhdHVzQgPgQQESNAoLY3JlYXRlX3RpbWUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMSNgoEdHlwZRgFIAEoDjIjLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uLlR5cGVCA+BBAxJOCgxtZW1vX2NvbW1lbnQYBiABKAsyMS5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbi5NZW1vQ29tbWVudFBheWxvYWRCA+BBA0gAEk4KDG1lbW9fbWVudGlvbhgHIAEoCzIxLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uLk1lbW9NZW50aW9uUGF5bG9hZEID4EEDSAAabAoSTWVtb0NvbW1lbnRQYXlsb2FkEgwKBG1lbW8YASABKAkSFAoMcmVsYXRlZF9tZW1vGAIgASgJEhQKDG1lbW9fc25pcHBldBgDIAEoCRIcChRyZWxhdGVkX21lbW9fc25pcHBldBgEIAEoCRpsChJNZW1vTWVudGlvblBheWxvYWQSDAoEbWVtbxgBIAEoCRIUCgxyZWxhdGVkX21lbW8YAiABKAkSFAoMbWVtb19zbmlwcGV0GAMgASgJEhwKFHJlbGF0ZWRfbWVtb19zbmlwcGV0GAQgASgJIjoKBlN0YXR1cxIWChJTVEFUVVNfVU5TUEVDSUZJRUQQABIKCgZVTlJFQUQQARIMCghBUkNISVZFRBACIkAKBFR5cGUSFAoQVFlQRV9VTlNQRUNJRklFRBAAEhAKDE1FTU9fQ09NTUVOVBABEhAKDE1FTU9fTUVOVElPThACOnDqQW0KHW1lbW9zLmFwaS52MS9Vc2VyTm90aWZpY2F0aW9uEil1c2Vycy97dXNlcn0vbm90aWZpY2F0aW9ucy97bm90aWZpY2F0aW9ufRoEbmFtZSoNbm90aWZpY2F0aW9uczIMbm90aWZpY2F0aW9uQgkKB3BheWxvYWQijwEKHExpc3RVc2VyTm90aWZpY2F0aW9uc1JlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhYKCXBhZ2Vfc2l6ZRgCIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAyABKAlCA+BBARITCgZmaWx0ZXIYBCABKAlCA+BBASJvCh1MaXN0VXNlck5vdGlmaWNhdGlvbnNSZXNwb25zZRI1Cg1ub3RpZmljYXRpb25zGAEgAygLMh4ubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJIpABCh1VcGRhdGVVc2VyTm90aWZpY2F0aW9uUmVxdWVzdBI5Cgxub3RpZmljYXRpb24YASABKAsyHi5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbkID4EECEjQKC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EECIlQKHURlbGV0ZVVzZXJOb3RpZmljYXRpb25SZXF1ZXN0EjMKBG5hbWUYASABKAlCJeBBAvpBHwodbWVtb3MuYXBpLnYxL1VzZXJOb3RpZmljYXRpb24ygBgKC1VzZXJTZXJ2aWNlEmMKCUxpc3RVc2VycxIeLm1lbW9zLmFwaS52MS5MaXN0VXNlcnNSZXF1ZXN0Gh8ubWVtb3MuYXBpLnYxLkxpc3RVc2Vyc1Jlc3BvbnNlIhWC0+STAg8SDS9hcGkvdjEvdXNlcnMSewoNQmF0Y2hHZXRVc2VycxIiLm1lbW9zLmFwaS52MS5CYXRjaEdldFVzZXJzUmVxdWVzdBojLm1lbW9zLmFwaS52MS5CYXRjaEdldFVzZXJzUmVzcG9uc2UiIYLT5JMCGzoBKiIWL2FwaS92MS91c2VyczpiYXRjaEdldBJiCgdHZXRVc2VyEhwubWVtb3MuYXBpLnYxLkdldFVzZXJSZXF1ZXN0GhIubWVtb3MuYXBpLnYxLlVzZXIiJdpBBG5hbWWC0+STAhgSFi9hcGkvdjEve25hbWU9dXNlcnMvKn0SZQoKQ3JlYXRlVXNlchIfLm1lbW9zLmFwaS52MS5DcmVhdGVVc2VyUmVxdWVzdBoSLm1lbW9zLmFwaS52MS5Vc2VyIiLaQQR1c2VygtPkkwIVOgR1c2VyIg0vYXBpL3YxL3VzZXJzEn8KClVwZGF0ZVVzZXISHy5tZW1vcy5hcGkudjEuVXBkYXRlVXNlclJlcXVlc3QaEi5tZW1vcy5hcGkudjEuVXNlciI82kEQdXNlcix1cGRhdGVfbWFza4LT5JMCIzoEdXNlcjIbL2FwaS92MS97dXNlci5uYW1lPXVzZXJzLyp9EmwKCkRlbGV0ZVVzZXISHy5tZW1vcy5hcGkudjEuRGVsZXRlVXNlclJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiJdpBBG5hbWWC0+STAhgqFi9hcGkvdjEve25hbWU9dXNlcnMvKn0SfgoQTGlzdEFsbFVzZXJTdGF0cxIlLm1lbW9zLmFwaS52MS5MaXN0QWxsVXNlclN0YXRzUmVxdWVzdBomLm1lbW9zLmFwaS52MS5MaXN0QWxsVXNlclN0YXRzUmVzcG9uc2UiG4LT5JMCFRITL2FwaS92MS91c2VyczpzdGF0cxJ6CgxHZXRVc2VyU3RhdHMSIS5tZW1vcy5hcGkudjEuR2V0VXNlclN0YXRzUmVxdWVzdBoXLm1lbW9zLmFwaS52MS5Vc2VyU3RhdHMiLtpBBG5hbWWC0+STAiESHy9hcGkvdjEve25hbWU9dXNlcnMvKn06Z2V0U3RhdHMSggEKDkdldFVzZXJTZXR0aW5nEiMubWVtb3MuYXBpLnYxLkdldFVzZXJTZXR0aW5nUmVxdWVzdBoZLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZyIw2kEEbmFtZYLT5JMCIxIhL2FwaS92MS97bmFtZT11c2Vycy8qL3NldHRpbmdzLyp9EqgBChFVcGRhdGVVc2VyU2V0dGluZxImLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyU2V0dGluZ1JlcXVlc3QaGS5tZW1vcy5hcGkudjEuVXNlclNldHRpbmciUNpBE3NldHRpbmcsdXBkYXRlX21hc2uC0+STAjQ6B3NldHRpbmcyKS9hcGkvdjEve3NldHRpbmcubmFtZT11c2Vycy8qL3NldHRpbmdzLyp9EpUBChBMaXN0VXNlclNldHRpbmdzEiUubWVtb3MuYXBpLnYxLkxpc3RVc2VyU2V0dGluZ3NSZXF1ZXN0GiYubWVtb3MuYXBpLnYxLkxpc3RVc2VyU2V0dGluZ3NSZXNwb25zZSIy2kEGcGFyZW50gtPkkwIjEiEvYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vc2V0dGluZ3MSuQEKGExpc3RQZXJzb25hbEFjY2Vzc1Rva2VucxItLm1lbW9zLmFwaS52MS5MaXN0UGVyc29uYWxBY2Nlc3NUb2tlbnNSZXF1ZXN0Gi4ubWVtb3MuYXBpLnYxLkxpc3RQZXJzb25hbEFjY2Vzc1Rva2Vuc1Jlc3BvbnNlIj7aQQZwYXJlbnSC0+STAi8SLS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS9wZXJzb25hbEFjY2Vzc1Rva2VucxK2AQoZQ3JlYXRlUGVyc29uYWxBY2Nlc3NUb2tlbhIuLm1lbW9zLmFwaS52MS5DcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVxdWVzdBovLm1lbW9zLmFwaS52MS5DcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVzcG9uc2UiOILT5JMCMjoBKiItL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3BlcnNvbmFsQWNjZXNzVG9rZW5zEqEBChlEZWxldGVQZXJzb25hbEFjY2Vzc1Rva2VuEi4ubWVtb3MuYXBpLnYxLkRlbGV0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IjzaQQRuYW1lgtPkkwIvKi0vYXBpL3YxL3tuYW1lPXVzZXJzLyovcGVyc29uYWxBY2Nlc3NUb2tlbnMvKn0SlQEKEExpc3RVc2VyV2ViaG9va3MSJS5tZW1vcy5hcGkudjEuTGlzdFVzZXJXZWJob29rc1JlcXVlc3QaJi5tZW1vcy5hcGkudjEuTGlzdFVzZXJXZWJob29rc1Jlc3BvbnNlIjLaQQZwYXJlbnSC0+STAiMSIS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS93ZWJob29rcxKbAQoRQ3JlYXRlVXNlcldlYmhvb2sSJi5tZW1vcy5hcGkudjEuQ3JlYXRlVXNlcldlYmhvb2tSZXF1ZXN0GhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rIkPaQQ5wYXJlbnQsd2ViaG9va4LT5JMCLDoHd2ViaG9vayIhL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3dlYmhvb2tzEqgBChFVcGRhdGVVc2VyV2ViaG9vaxImLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyV2ViaG9va1JlcXVlc3QaGS5tZW1vcy5hcGkudjEuVXNlcldlYmhvb2siUNpBE3dlYmhvb2ssdXBkYXRlX21hc2uC0+STAjQ6B3dlYmhvb2syKS9hcGkvdjEve3dlYmhvb2submFtZT11c2Vycy8qL3dlYmhvb2tzLyp9EoUBChFEZWxldGVVc2VyV2ViaG9vaxImLm1lbW9zLmFwaS52MS5EZWxldGVVc2VyV2ViaG9va1JlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiMNpBBG5hbWWC0+STAiMqIS9hcGkvdjEve25hbWU9dXNlcnMvKi93ZWJob29rcy8qfRKpAQoVTGlzdFVzZXJOb3RpZmljYXRpb25zEioubWVtb3MuYXBpLnYxLkxpc3RVc2VyTm90aWZpY2F0aW9uc1JlcXVlc3QaKy5tZW1vcy5hcGkudjEuTGlzdFVzZXJOb3RpZmljYXRpb25zUmVzcG9uc2UiN9pBBnBhcmVudILT5JMCKBImL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L25vdGlmaWNhdGlvbnMSywEKFlVwZGF0ZVVzZXJOb3RpZmljYXRpb24SKy5tZW1vcy5hcGkudjEuVXBkYXRlVXNlck5vdGlmaWNhdGlvblJlcXVlc3QaHi5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbiJk2kEYbm90aWZpY2F0aW9uLHVwZGF0ZV9tYXNrgtPkkwJDOgxub3RpZmljYXRpb24yMy9hcGkvdjEve25vdGlmaWNhdGlvbi5uYW1lPXVzZXJzLyovbm90aWZpY2F0aW9ucy8qfRKUAQoWRGVsZXRlVXNlck5vdGlmaWNhdGlvbhIrLm1lbW9zLmFwaS52MS5EZWxldGVVc2VyTm90aWZpY2F0aW9uUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSI12kEEbmFtZYLT5JMCKComL2FwaS92MS97bmFtZT11c2Vycy8qL25vdGlmaWNhdGlvbnMvKn1CqAEKEGNvbS5tZW1vcy5hcGkudjFCEFVzZXJTZXJ2aWNlUHJvdG9QAVowZ2l0aHViLmNvbS91c2VtZW1vcy9tZW1vcy9wcm90by9nZW4vYXBpL3YxO2FwaXYxogIDTUFYqgIMTWVtb3MuQXBpLlYxygIMTWVtb3NcQXBpXFYx4gIYTWVtb3NcQXBpXFYxXEdQQk1ldGFkYXRh6gIOTWVtb3M6OkFwaTo6VjFiBnByb3RvMw", [file_api_v1_common, file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp]); fileDesc("ChlhcGkvdjEvdXNlcl9zZXJ2aWNlLnByb3RvEgxtZW1vcy5hcGkudjEi1gMKBFVzZXISEQoEbmFtZRgBIAEoCUID4EEIEioKBHJvbGUYAiABKA4yFy5tZW1vcy5hcGkudjEuVXNlci5Sb2xlQgPgQQISFQoIdXNlcm5hbWUYAyABKAlCA+BBAhISCgVlbWFpbBgEIAEoCUID4EEBEhkKDGRpc3BsYXlfbmFtZRgFIAEoCUID4EEBEhcKCmF2YXRhcl91cmwYBiABKAlCA+BBARIYCgtkZXNjcmlwdGlvbhgHIAEoCUID4EEBEhUKCHBhc3N3b3JkGAggASgJQgPgQQQSJwoFc3RhdGUYCSABKA4yEy5tZW1vcy5hcGkudjEuU3RhdGVCA+BBAhI0CgtjcmVhdGVfdGltZRgKIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxI0Cgt1cGRhdGVfdGltZRgLIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAyIxCgRSb2xlEhQKEFJPTEVfVU5TUEVDSUZJRUQQABIJCgVBRE1JThACEggKBFVTRVIQAzo36kE0ChFtZW1vcy5hcGkudjEvVXNlchIMdXNlcnMve3VzZXJ9GgRuYW1lKgV1c2VyczIEdXNlciJzChBMaXN0VXNlcnNSZXF1ZXN0EhYKCXBhZ2Vfc2l6ZRgBIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAiABKAlCA+BBARITCgZmaWx0ZXIYAyABKAlCA+BBARIZCgxzaG93X2RlbGV0ZWQYBCABKAhCA+BBASJjChFMaXN0VXNlcnNSZXNwb25zZRIhCgV1c2VycxgBIAMoCzISLm1lbW9zLmFwaS52MS5Vc2VyEhcKD25leHRfcGFnZV90b2tlbhgCIAEoCRISCgp0b3RhbF9zaXplGAMgASgFIikKFEJhdGNoR2V0VXNlcnNSZXF1ZXN0EhEKCXVzZXJuYW1lcxgBIAMoCSI6ChVCYXRjaEdldFVzZXJzUmVzcG9uc2USIQoFdXNlcnMYASADKAsyEi5tZW1vcy5hcGkudjEuVXNlciJtCg5HZXRVc2VyUmVxdWVzdBInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEjIKCXJlYWRfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2tCA+BBASKIAQoRQ3JlYXRlVXNlclJlcXVlc3QSKAoEdXNlchgBIAEoCzISLm1lbW9zLmFwaS52MS5Vc2VyQgbgQQLgQQQSFAoHdXNlcl9pZBgCIAEoCUID4EEBEhoKDXZhbGlkYXRlX29ubHkYAyABKAhCA+BBARIXCgpyZXF1ZXN0X2lkGAQgASgJQgPgQQEijAEKEVVwZGF0ZVVzZXJSZXF1ZXN0EiUKBHVzZXIYASABKAsyEi5tZW1vcy5hcGkudjEuVXNlckID4EECEjQKC3VwZGF0ZV9tYXNrGAIgASgLMhouZ29vZ2xlLnByb3RvYnVmLkZpZWxkTWFza0ID4EECEhoKDWFsbG93X21pc3NpbmcYAyABKAhCA+BBASJQChFEZWxldGVVc2VyUmVxdWVzdBInCgRuYW1lGAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhIKBWZvcmNlGAIgASgIQgPgQQEi2AMKCVVzZXJTdGF0cxIRCgRuYW1lGAEgASgJQgPgQQgSOwoXbWVtb19kaXNwbGF5X3RpbWVzdGFtcHMYAiADKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEj4KD21lbW9fdHlwZV9zdGF0cxgDIAEoCzIlLm1lbW9zLmFwaS52MS5Vc2VyU3RhdHMuTWVtb1R5cGVTdGF0cxI4Cgl0YWdfY291bnQYBCADKAsyJS5tZW1vcy5hcGkudjEuVXNlclN0YXRzLlRhZ0NvdW50RW50cnkSFAoMcGlubmVkX21lbW9zGAUgAygJEhgKEHRvdGFsX21lbW9fY291bnQYBiABKAUaXwoNTWVtb1R5cGVTdGF0cxISCgpsaW5rX2NvdW50GAEgASgFEhIKCmNvZGVfY291bnQYAiABKAUSEgoKdG9kb19jb3VudBgDIAEoBRISCgp1bmRvX2NvdW50GAQgASgFGi8KDVRhZ0NvdW50RW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgFOgI4ATo/6kE8ChZtZW1vcy5hcGkudjEvVXNlclN0YXRzEgx1c2Vycy97dXNlcn0qCXVzZXJTdGF0czIJdXNlclN0YXRzIj4KE0dldFVzZXJTdGF0c1JlcXVlc3QSJwoEbmFtZRgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlciIZChdMaXN0QWxsVXNlclN0YXRzUmVxdWVzdCJCChhMaXN0QWxsVXNlclN0YXRzUmVzcG9uc2USJgoFc3RhdHMYASADKAsyFy5tZW1vcy5hcGkudjEuVXNlclN0YXRzIuQDCgtVc2VyU2V0dGluZxIRCgRuYW1lGAEgASgJQgPgQQgSQwoPZ2VuZXJhbF9zZXR0aW5nGAIgASgLMigubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nLkdlbmVyYWxTZXR0aW5nSAASRQoQd2ViaG9va3Nfc2V0dGluZxgFIAEoCzIpLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZy5XZWJob29rc1NldHRpbmdIABpXCg5HZW5lcmFsU2V0dGluZxITCgZsb2NhbGUYASABKAlCA+BBARIcCg9tZW1vX3Zpc2liaWxpdHkYAyABKAlCA+BBARISCgV0aGVtZRgEIAEoCUID4EEBGj4KD1dlYmhvb2tzU2V0dGluZxIrCgh3ZWJob29rcxgBIAMoCzIZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9vayI1CgNLZXkSEwoPS0VZX1VOU1BFQ0lGSUVEEAASCwoHR0VORVJBTBABEgwKCFdFQkhPT0tTEAQ6XepBWgoYbWVtb3MuYXBpLnYxL1VzZXJTZXR0aW5nEiN1c2Vycy97dXNlcm5hbWV9L3NldHRpbmdzL3tzZXR0aW5nfSoMdXNlclNldHRpbmdzMgt1c2VyU2V0dGluZ0IHCgV2YWx1ZSJHChVHZXRVc2VyU2V0dGluZ1JlcXVlc3QSLgoEbmFtZRgBIAEoCUIg4EEC+kEaChhtZW1vcy5hcGkudjEvVXNlclNldHRpbmcigQEKGFVwZGF0ZVVzZXJTZXR0aW5nUmVxdWVzdBIvCgdzZXR0aW5nGAEgASgLMhkubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nQgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQIidQoXTGlzdFVzZXJTZXR0aW5nc1JlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEhYKCXBhZ2Vfc2l6ZRgCIAEoBUID4EEBEhcKCnBhZ2VfdG9rZW4YAyABKAlCA+BBASJ0ChhMaXN0VXNlclNldHRpbmdzUmVzcG9uc2USKwoIc2V0dGluZ3MYASADKAsyGS5tZW1vcy5hcGkudjEuVXNlclNldHRpbmcSFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhIKCnRvdGFsX3NpemUYAyABKAUi6gEKDkxpbmtlZElkZW50aXR5EhEKBG5hbWUYASABKAlCA+BBCBI3CghpZHBfbmFtZRgCIAEoCUIl4EED+kEfCh1tZW1vcy5hcGkudjEvSWRlbnRpdHlQcm92aWRlchIXCgpleHRlcm5fdWlkGAMgASgJQgPgQQM6c+pBcAobbWVtb3MuYXBpLnYxL0xpbmtlZElkZW50aXR5Ei91c2Vycy97dXNlcn0vbGlua2VkSWRlbnRpdGllcy97bGlua2VkX2lkZW50aXR5fSoQbGlua2VkSWRlbnRpdGllczIObGlua2VkSWRlbnRpdHkiSAobTGlzdExpbmtlZElkZW50aXRpZXNSZXF1ZXN0EikKBnBhcmVudBgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlciJXChxMaXN0TGlua2VkSWRlbnRpdGllc1Jlc3BvbnNlEjcKEWxpbmtlZF9pZGVudGl0aWVzGAEgAygLMhwubWVtb3MuYXBpLnYxLkxpbmtlZElkZW50aXR5IssBChtDcmVhdGVMaW5rZWRJZGVudGl0eVJlcXVlc3QSKQoGcGFyZW50GAEgASgJQhngQQL6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEjcKCGlkcF9uYW1lGAIgASgJQiXgQQL6QR8KHW1lbW9zLmFwaS52MS9JZGVudGl0eVByb3ZpZGVyEhEKBGNvZGUYAyABKAlCA+BBAhIZCgxyZWRpcmVjdF91cmkYBCABKAlCA+BBAhIaCg1jb2RlX3ZlcmlmaWVyGAUgASgJQgPgQQEiTQoYR2V0TGlua2VkSWRlbnRpdHlSZXF1ZXN0EjEKBG5hbWUYASABKAlCI+BBAvpBHQobbWVtb3MuYXBpLnYxL0xpbmtlZElkZW50aXR5IlAKG0RlbGV0ZUxpbmtlZElkZW50aXR5UmVxdWVzdBIxCgRuYW1lGAEgASgJQiPgQQL6QR0KG21lbW9zLmFwaS52MS9MaW5rZWRJZGVudGl0eSLyAgoTUGVyc29uYWxBY2Nlc3NUb2tlbhIRCgRuYW1lGAEgASgJQgPgQQgSGAoLZGVzY3JpcHRpb24YAiABKAlCA+BBARIzCgpjcmVhdGVkX2F0GAMgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDEjMKCmV4cGlyZXNfYXQYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQESNQoMbGFzdF91c2VkX2F0GAUgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEID4EEDOowB6kGIAQogbWVtb3MuYXBpLnYxL1BlcnNvbmFsQWNjZXNzVG9rZW4SOXVzZXJzL3t1c2VyfS9wZXJzb25hbEFjY2Vzc1Rva2Vucy97cGVyc29uYWxfYWNjZXNzX3Rva2VufSoUcGVyc29uYWxBY2Nlc3NUb2tlbnMyE3BlcnNvbmFsQWNjZXNzVG9rZW4ifQofTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISFgoJcGFnZV9zaXplGAIgASgFQgPgQQESFwoKcGFnZV90b2tlbhgDIAEoCUID4EEBIpIBCiBMaXN0UGVyc29uYWxBY2Nlc3NUb2tlbnNSZXNwb25zZRJBChZwZXJzb25hbF9hY2Nlc3NfdG9rZW5zGAEgAygLMiEubWVtb3MuYXBpLnYxLlBlcnNvbmFsQWNjZXNzVG9rZW4SFwoPbmV4dF9wYWdlX3Rva2VuGAIgASgJEhIKCnRvdGFsX3NpemUYAyABKAUihQEKIENyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXF1ZXN0EikKBnBhcmVudBgBIAEoCUIZ4EEC+kETChFtZW1vcy5hcGkudjEvVXNlchIYCgtkZXNjcmlwdGlvbhgCIAEoCUID4EEBEhwKD2V4cGlyZXNfaW5fZGF5cxgDIAEoBUID4EEBInQKIUNyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXNwb25zZRJAChVwZXJzb25hbF9hY2Nlc3NfdG9rZW4YASABKAsyIS5tZW1vcy5hcGkudjEuUGVyc29uYWxBY2Nlc3NUb2tlbhINCgV0b2tlbhgCIAEoCSJaCiBEZWxldGVQZXJzb25hbEFjY2Vzc1Rva2VuUmVxdWVzdBI2CgRuYW1lGAEgASgJQijgQQL6QSIKIG1lbW9zLmFwaS52MS9QZXJzb25hbEFjY2Vzc1Rva2VuIqoBCgtVc2VyV2ViaG9vaxIMCgRuYW1lGAEgASgJEgsKA3VybBgCIAEoCRIUCgxkaXNwbGF5X25hbWUYAyABKAkSNAoLY3JlYXRlX3RpbWUYBCABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMSNAoLdXBkYXRlX3RpbWUYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wQgPgQQMiLgoXTGlzdFVzZXJXZWJob29rc1JlcXVlc3QSEwoGcGFyZW50GAEgASgJQgPgQQIiRwoYTGlzdFVzZXJXZWJob29rc1Jlc3BvbnNlEisKCHdlYmhvb2tzGAEgAygLMhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rImAKGENyZWF0ZVVzZXJXZWJob29rUmVxdWVzdBITCgZwYXJlbnQYASABKAlCA+BBAhIvCgd3ZWJob29rGAIgASgLMhkubWVtb3MuYXBpLnYxLlVzZXJXZWJob29rQgPgQQIifAoYVXBkYXRlVXNlcldlYmhvb2tSZXF1ZXN0Ei8KB3dlYmhvb2sYASABKAsyGS5tZW1vcy5hcGkudjEuVXNlcldlYmhvb2tCA+BBAhIvCgt1cGRhdGVfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2siLQoYRGVsZXRlVXNlcldlYmhvb2tSZXF1ZXN0EhEKBG5hbWUYASABKAlCA+BBAiKiBwoQVXNlck5vdGlmaWNhdGlvbhIUCgRuYW1lGAEgASgJQgbgQQPgQQgSKQoGc2VuZGVyGAIgASgJQhngQQP6QRMKEW1lbW9zLmFwaS52MS9Vc2VyEiwKC3NlbmRlcl91c2VyGAggASgLMhIubWVtb3MuYXBpLnYxLlVzZXJCA+BBAxI6CgZzdGF0dXMYAyABKA4yJS5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbi5TdGF0dXNCA+BBARI0CgtjcmVhdGVfdGltZRgEIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBCA+BBAxI2CgR0eXBlGAUgASgOMiMubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24uVHlwZUID4EEDEk4KDG1lbW9fY29tbWVudBgGIAEoCzIxLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uLk1lbW9Db21tZW50UGF5bG9hZEID4EEDSAASTgoMbWVtb19tZW50aW9uGAcgASgLMjEubWVtb3MuYXBpLnYxLlVzZXJOb3RpZmljYXRpb24uTWVtb01lbnRpb25QYXlsb2FkQgPgQQNIABpsChJNZW1vQ29tbWVudFBheWxvYWQSDAoEbWVtbxgBIAEoCRIUCgxyZWxhdGVkX21lbW8YAiABKAkSFAoMbWVtb19zbmlwcGV0GAMgASgJEhwKFHJlbGF0ZWRfbWVtb19zbmlwcGV0GAQgASgJGmwKEk1lbW9NZW50aW9uUGF5bG9hZBIMCgRtZW1vGAEgASgJEhQKDHJlbGF0ZWRfbWVtbxgCIAEoCRIUCgxtZW1vX3NuaXBwZXQYAyABKAkSHAoUcmVsYXRlZF9tZW1vX3NuaXBwZXQYBCABKAkiOgoGU3RhdHVzEhYKElNUQVRVU19VTlNQRUNJRklFRBAAEgoKBlVOUkVBRBABEgwKCEFSQ0hJVkVEEAIiQAoEVHlwZRIUChBUWVBFX1VOU1BFQ0lGSUVEEAASEAoMTUVNT19DT01NRU5UEAESEAoMTUVNT19NRU5USU9OEAI6cOpBbQodbWVtb3MuYXBpLnYxL1VzZXJOb3RpZmljYXRpb24SKXVzZXJzL3t1c2VyfS9ub3RpZmljYXRpb25zL3tub3RpZmljYXRpb259GgRuYW1lKg1ub3RpZmljYXRpb25zMgxub3RpZmljYXRpb25CCQoHcGF5bG9hZCKPAQocTGlzdFVzZXJOb3RpZmljYXRpb25zUmVxdWVzdBIpCgZwYXJlbnQYASABKAlCGeBBAvpBEwoRbWVtb3MuYXBpLnYxL1VzZXISFgoJcGFnZV9zaXplGAIgASgFQgPgQQESFwoKcGFnZV90b2tlbhgDIAEoCUID4EEBEhMKBmZpbHRlchgEIAEoCUID4EEBIm8KHUxpc3RVc2VyTm90aWZpY2F0aW9uc1Jlc3BvbnNlEjUKDW5vdGlmaWNhdGlvbnMYASADKAsyHi5tZW1vcy5hcGkudjEuVXNlck5vdGlmaWNhdGlvbhIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkikAEKHVVwZGF0ZVVzZXJOb3RpZmljYXRpb25SZXF1ZXN0EjkKDG5vdGlmaWNhdGlvbhgBIAEoCzIeLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uQgPgQQISNAoLdXBkYXRlX21hc2sYAiABKAsyGi5nb29nbGUucHJvdG9idWYuRmllbGRNYXNrQgPgQQIiVAodRGVsZXRlVXNlck5vdGlmaWNhdGlvblJlcXVlc3QSMwoEbmFtZRgBIAEoCUIl4EEC+kEfCh1tZW1vcy5hcGkudjEvVXNlck5vdGlmaWNhdGlvbjKCHQoLVXNlclNlcnZpY2USYwoJTGlzdFVzZXJzEh4ubWVtb3MuYXBpLnYxLkxpc3RVc2Vyc1JlcXVlc3QaHy5tZW1vcy5hcGkudjEuTGlzdFVzZXJzUmVzcG9uc2UiFYLT5JMCDxINL2FwaS92MS91c2VycxJ7Cg1CYXRjaEdldFVzZXJzEiIubWVtb3MuYXBpLnYxLkJhdGNoR2V0VXNlcnNSZXF1ZXN0GiMubWVtb3MuYXBpLnYxLkJhdGNoR2V0VXNlcnNSZXNwb25zZSIhgtPkkwIbOgEqIhYvYXBpL3YxL3VzZXJzOmJhdGNoR2V0EmIKB0dldFVzZXISHC5tZW1vcy5hcGkudjEuR2V0VXNlclJlcXVlc3QaEi5tZW1vcy5hcGkudjEuVXNlciIl2kEEbmFtZYLT5JMCGBIWL2FwaS92MS97bmFtZT11c2Vycy8qfRJlCgpDcmVhdGVVc2VyEh8ubWVtb3MuYXBpLnYxLkNyZWF0ZVVzZXJSZXF1ZXN0GhIubWVtb3MuYXBpLnYxLlVzZXIiItpBBHVzZXKC0+STAhU6BHVzZXIiDS9hcGkvdjEvdXNlcnMSfwoKVXBkYXRlVXNlchIfLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyUmVxdWVzdBoSLm1lbW9zLmFwaS52MS5Vc2VyIjzaQRB1c2VyLHVwZGF0ZV9tYXNrgtPkkwIjOgR1c2VyMhsvYXBpL3YxL3t1c2VyLm5hbWU9dXNlcnMvKn0SbAoKRGVsZXRlVXNlchIfLm1lbW9zLmFwaS52MS5EZWxldGVVc2VyUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIl2kEEbmFtZYLT5JMCGCoWL2FwaS92MS97bmFtZT11c2Vycy8qfRJ+ChBMaXN0QWxsVXNlclN0YXRzEiUubWVtb3MuYXBpLnYxLkxpc3RBbGxVc2VyU3RhdHNSZXF1ZXN0GiYubWVtb3MuYXBpLnYxLkxpc3RBbGxVc2VyU3RhdHNSZXNwb25zZSIbgtPkkwIVEhMvYXBpL3YxL3VzZXJzOnN0YXRzEnoKDEdldFVzZXJTdGF0cxIhLm1lbW9zLmFwaS52MS5HZXRVc2VyU3RhdHNSZXF1ZXN0GhcubWVtb3MuYXBpLnYxLlVzZXJTdGF0cyIu2kEEbmFtZYLT5JMCIRIfL2FwaS92MS97bmFtZT11c2Vycy8qfTpnZXRTdGF0cxKCAQoOR2V0VXNlclNldHRpbmcSIy5tZW1vcy5hcGkudjEuR2V0VXNlclNldHRpbmdSZXF1ZXN0GhkubWVtb3MuYXBpLnYxLlVzZXJTZXR0aW5nIjDaQQRuYW1lgtPkkwIjEiEvYXBpL3YxL3tuYW1lPXVzZXJzLyovc2V0dGluZ3MvKn0SqAEKEVVwZGF0ZVVzZXJTZXR0aW5nEiYubWVtb3MuYXBpLnYxLlVwZGF0ZVVzZXJTZXR0aW5nUmVxdWVzdBoZLm1lbW9zLmFwaS52MS5Vc2VyU2V0dGluZyJQ2kETc2V0dGluZyx1cGRhdGVfbWFza4LT5JMCNDoHc2V0dGluZzIpL2FwaS92MS97c2V0dGluZy5uYW1lPXVzZXJzLyovc2V0dGluZ3MvKn0SlQEKEExpc3RVc2VyU2V0dGluZ3MSJS5tZW1vcy5hcGkudjEuTGlzdFVzZXJTZXR0aW5nc1JlcXVlc3QaJi5tZW1vcy5hcGkudjEuTGlzdFVzZXJTZXR0aW5nc1Jlc3BvbnNlIjLaQQZwYXJlbnSC0+STAiMSIS9hcGkvdjEve3BhcmVudD11c2Vycy8qfS9zZXR0aW5ncxKpAQoUTGlzdExpbmtlZElkZW50aXRpZXMSKS5tZW1vcy5hcGkudjEuTGlzdExpbmtlZElkZW50aXRpZXNSZXF1ZXN0GioubWVtb3MuYXBpLnYxLkxpc3RMaW5rZWRJZGVudGl0aWVzUmVzcG9uc2UiOtpBBnBhcmVudILT5JMCKxIpL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L2xpbmtlZElkZW50aXRpZXMSpwEKFENyZWF0ZUxpbmtlZElkZW50aXR5EikubWVtb3MuYXBpLnYxLkNyZWF0ZUxpbmtlZElkZW50aXR5UmVxdWVzdBocLm1lbW9zLmFwaS52MS5MaW5rZWRJZGVudGl0eSJG2kEPcGFyZW50LGlkcF9uYW1lgtPkkwIuOgEqIikvYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vbGlua2VkSWRlbnRpdGllcxKTAQoRR2V0TGlua2VkSWRlbnRpdHkSJi5tZW1vcy5hcGkudjEuR2V0TGlua2VkSWRlbnRpdHlSZXF1ZXN0GhwubWVtb3MuYXBpLnYxLkxpbmtlZElkZW50aXR5IjjaQQRuYW1lgtPkkwIrEikvYXBpL3YxL3tuYW1lPXVzZXJzLyovbGlua2VkSWRlbnRpdGllcy8qfRKTAQoURGVsZXRlTGlua2VkSWRlbnRpdHkSKS5tZW1vcy5hcGkudjEuRGVsZXRlTGlua2VkSWRlbnRpdHlSZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IjjaQQRuYW1lgtPkkwIrKikvYXBpL3YxL3tuYW1lPXVzZXJzLyovbGlua2VkSWRlbnRpdGllcy8qfRK5AQoYTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zEi0ubWVtb3MuYXBpLnYxLkxpc3RQZXJzb25hbEFjY2Vzc1Rva2Vuc1JlcXVlc3QaLi5tZW1vcy5hcGkudjEuTGlzdFBlcnNvbmFsQWNjZXNzVG9rZW5zUmVzcG9uc2UiPtpBBnBhcmVudILT5JMCLxItL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3BlcnNvbmFsQWNjZXNzVG9rZW5zErYBChlDcmVhdGVQZXJzb25hbEFjY2Vzc1Rva2VuEi4ubWVtb3MuYXBpLnYxLkNyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXF1ZXN0Gi8ubWVtb3MuYXBpLnYxLkNyZWF0ZVBlcnNvbmFsQWNjZXNzVG9rZW5SZXNwb25zZSI4gtPkkwIyOgEqIi0vYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vcGVyc29uYWxBY2Nlc3NUb2tlbnMSoQEKGURlbGV0ZVBlcnNvbmFsQWNjZXNzVG9rZW4SLi5tZW1vcy5hcGkudjEuRGVsZXRlUGVyc29uYWxBY2Nlc3NUb2tlblJlcXVlc3QaFi5nb29nbGUucHJvdG9idWYuRW1wdHkiPNpBBG5hbWWC0+STAi8qLS9hcGkvdjEve25hbWU9dXNlcnMvKi9wZXJzb25hbEFjY2Vzc1Rva2Vucy8qfRKVAQoQTGlzdFVzZXJXZWJob29rcxIlLm1lbW9zLmFwaS52MS5MaXN0VXNlcldlYmhvb2tzUmVxdWVzdBomLm1lbW9zLmFwaS52MS5MaXN0VXNlcldlYmhvb2tzUmVzcG9uc2UiMtpBBnBhcmVudILT5JMCIxIhL2FwaS92MS97cGFyZW50PXVzZXJzLyp9L3dlYmhvb2tzEpsBChFDcmVhdGVVc2VyV2ViaG9vaxImLm1lbW9zLmFwaS52MS5DcmVhdGVVc2VyV2ViaG9va1JlcXVlc3QaGS5tZW1vcy5hcGkudjEuVXNlcldlYmhvb2siQ9pBDnBhcmVudCx3ZWJob29rgtPkkwIsOgd3ZWJob29rIiEvYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vd2ViaG9va3MSqAEKEVVwZGF0ZVVzZXJXZWJob29rEiYubWVtb3MuYXBpLnYxLlVwZGF0ZVVzZXJXZWJob29rUmVxdWVzdBoZLm1lbW9zLmFwaS52MS5Vc2VyV2ViaG9vayJQ2kETd2ViaG9vayx1cGRhdGVfbWFza4LT5JMCNDoHd2ViaG9vazIpL2FwaS92MS97d2ViaG9vay5uYW1lPXVzZXJzLyovd2ViaG9va3MvKn0ShQEKEURlbGV0ZVVzZXJXZWJob29rEiYubWVtb3MuYXBpLnYxLkRlbGV0ZVVzZXJXZWJob29rUmVxdWVzdBoWLmdvb2dsZS5wcm90b2J1Zi5FbXB0eSIw2kEEbmFtZYLT5JMCIyohL2FwaS92MS97bmFtZT11c2Vycy8qL3dlYmhvb2tzLyp9EqkBChVMaXN0VXNlck5vdGlmaWNhdGlvbnMSKi5tZW1vcy5hcGkudjEuTGlzdFVzZXJOb3RpZmljYXRpb25zUmVxdWVzdBorLm1lbW9zLmFwaS52MS5MaXN0VXNlck5vdGlmaWNhdGlvbnNSZXNwb25zZSI32kEGcGFyZW50gtPkkwIoEiYvYXBpL3YxL3twYXJlbnQ9dXNlcnMvKn0vbm90aWZpY2F0aW9ucxLLAQoWVXBkYXRlVXNlck5vdGlmaWNhdGlvbhIrLm1lbW9zLmFwaS52MS5VcGRhdGVVc2VyTm90aWZpY2F0aW9uUmVxdWVzdBoeLm1lbW9zLmFwaS52MS5Vc2VyTm90aWZpY2F0aW9uImTaQRhub3RpZmljYXRpb24sdXBkYXRlX21hc2uC0+STAkM6DG5vdGlmaWNhdGlvbjIzL2FwaS92MS97bm90aWZpY2F0aW9uLm5hbWU9dXNlcnMvKi9ub3RpZmljYXRpb25zLyp9EpQBChZEZWxldGVVc2VyTm90aWZpY2F0aW9uEisubWVtb3MuYXBpLnYxLkRlbGV0ZVVzZXJOb3RpZmljYXRpb25SZXF1ZXN0GhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5IjXaQQRuYW1lgtPkkwIoKiYvYXBpL3YxL3tuYW1lPXVzZXJzLyovbm90aWZpY2F0aW9ucy8qfUKoAQoQY29tLm1lbW9zLmFwaS52MUIQVXNlclNlcnZpY2VQcm90b1ABWjBnaXRodWIuY29tL3VzZW1lbW9zL21lbW9zL3Byb3RvL2dlbi9hcGkvdjE7YXBpdjGiAgNNQViqAgxNZW1vcy5BcGkuVjHKAgxNZW1vc1xBcGlcVjHiAhhNZW1vc1xBcGlcVjFcR1BCTWV0YWRhdGHqAg5NZW1vczo6QXBpOjpWMWIGcHJvdG8z", [file_api_v1_common, file_google_api_annotations, file_google_api_client, file_google_api_field_behavior, file_google_api_resource, file_google_protobuf_empty, file_google_protobuf_field_mask, file_google_protobuf_timestamp]);
/** /**
* @generated from message memos.api.v1.User * @generated from message memos.api.v1.User
...@@ -785,6 +785,171 @@ export type ListUserSettingsResponse = Message<"memos.api.v1.ListUserSettingsRes ...@@ -785,6 +785,171 @@ export type ListUserSettingsResponse = Message<"memos.api.v1.ListUserSettingsRes
export const ListUserSettingsResponseSchema: GenMessage<ListUserSettingsResponse> = /*@__PURE__*/ export const ListUserSettingsResponseSchema: GenMessage<ListUserSettingsResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 17); messageDesc(file_api_v1_user_service, 17);
/**
* LinkedIdentity represents an SSO identity linked to a user account.
*
* @generated from message memos.api.v1.LinkedIdentity
*/
export type LinkedIdentity = Message<"memos.api.v1.LinkedIdentity"> & {
/**
* The resource name of the linked identity.
* Format: users/{user}/linkedIdentities/{linked_identity}
*
* @generated from field: string name = 1;
*/
name: string;
/**
* The resource name of the identity provider.
* Format: identity-providers/{uid}
*
* @generated from field: string idp_name = 2;
*/
idpName: string;
/**
* The external user identifier from the identity provider.
*
* @generated from field: string extern_uid = 3;
*/
externUid: string;
};
/**
* Describes the message memos.api.v1.LinkedIdentity.
* Use `create(LinkedIdentitySchema)` to create a new message.
*/
export const LinkedIdentitySchema: GenMessage<LinkedIdentity> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 18);
/**
* @generated from message memos.api.v1.ListLinkedIdentitiesRequest
*/
export type ListLinkedIdentitiesRequest = Message<"memos.api.v1.ListLinkedIdentitiesRequest"> & {
/**
* Required. The parent resource whose linked identities will be listed.
* Format: users/{user}
*
* @generated from field: string parent = 1;
*/
parent: string;
};
/**
* Describes the message memos.api.v1.ListLinkedIdentitiesRequest.
* Use `create(ListLinkedIdentitiesRequestSchema)` to create a new message.
*/
export const ListLinkedIdentitiesRequestSchema: GenMessage<ListLinkedIdentitiesRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 19);
/**
* @generated from message memos.api.v1.ListLinkedIdentitiesResponse
*/
export type ListLinkedIdentitiesResponse = Message<"memos.api.v1.ListLinkedIdentitiesResponse"> & {
/**
* The list of linked identities.
*
* @generated from field: repeated memos.api.v1.LinkedIdentity linked_identities = 1;
*/
linkedIdentities: LinkedIdentity[];
};
/**
* Describes the message memos.api.v1.ListLinkedIdentitiesResponse.
* Use `create(ListLinkedIdentitiesResponseSchema)` to create a new message.
*/
export const ListLinkedIdentitiesResponseSchema: GenMessage<ListLinkedIdentitiesResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 20);
/**
* @generated from message memos.api.v1.CreateLinkedIdentityRequest
*/
export type CreateLinkedIdentityRequest = Message<"memos.api.v1.CreateLinkedIdentityRequest"> & {
/**
* Required. The parent user who owns the linked identity.
* Format: users/{user}
*
* @generated from field: string parent = 1;
*/
parent: string;
/**
* Required. The identity provider to link.
* Format: identity-providers/{uid}
*
* @generated from field: string idp_name = 2;
*/
idpName: string;
/**
* Required. The authorization code from the identity provider.
*
* @generated from field: string code = 3;
*/
code: string;
/**
* Required. The redirect URI used in the OAuth flow.
*
* @generated from field: string redirect_uri = 4;
*/
redirectUri: string;
/**
* Optional. The PKCE code verifier used in the OAuth flow.
*
* @generated from field: string code_verifier = 5;
*/
codeVerifier: string;
};
/**
* Describes the message memos.api.v1.CreateLinkedIdentityRequest.
* Use `create(CreateLinkedIdentityRequestSchema)` to create a new message.
*/
export const CreateLinkedIdentityRequestSchema: GenMessage<CreateLinkedIdentityRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 21);
/**
* @generated from message memos.api.v1.GetLinkedIdentityRequest
*/
export type GetLinkedIdentityRequest = Message<"memos.api.v1.GetLinkedIdentityRequest"> & {
/**
* Required. The resource name of the linked identity to get.
* Format: users/{user}/linkedIdentities/{linked_identity}
*
* @generated from field: string name = 1;
*/
name: string;
};
/**
* Describes the message memos.api.v1.GetLinkedIdentityRequest.
* Use `create(GetLinkedIdentityRequestSchema)` to create a new message.
*/
export const GetLinkedIdentityRequestSchema: GenMessage<GetLinkedIdentityRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 22);
/**
* @generated from message memos.api.v1.DeleteLinkedIdentityRequest
*/
export type DeleteLinkedIdentityRequest = Message<"memos.api.v1.DeleteLinkedIdentityRequest"> & {
/**
* Required. The resource name of the linked identity to delete.
* Format: users/{user}/linkedIdentities/{linked_identity}
*
* @generated from field: string name = 1;
*/
name: string;
};
/**
* Describes the message memos.api.v1.DeleteLinkedIdentityRequest.
* Use `create(DeleteLinkedIdentityRequestSchema)` to create a new message.
*/
export const DeleteLinkedIdentityRequestSchema: GenMessage<DeleteLinkedIdentityRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 23);
/** /**
* PersonalAccessToken represents a long-lived token for API/script access. * PersonalAccessToken represents a long-lived token for API/script access.
* PATs are distinct from short-lived JWT access tokens used for session authentication. * PATs are distinct from short-lived JWT access tokens used for session authentication.
...@@ -834,7 +999,7 @@ export type PersonalAccessToken = Message<"memos.api.v1.PersonalAccessToken"> & ...@@ -834,7 +999,7 @@ export type PersonalAccessToken = Message<"memos.api.v1.PersonalAccessToken"> &
* Use `create(PersonalAccessTokenSchema)` to create a new message. * Use `create(PersonalAccessTokenSchema)` to create a new message.
*/ */
export const PersonalAccessTokenSchema: GenMessage<PersonalAccessToken> = /*@__PURE__*/ export const PersonalAccessTokenSchema: GenMessage<PersonalAccessToken> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 18); messageDesc(file_api_v1_user_service, 24);
/** /**
* @generated from message memos.api.v1.ListPersonalAccessTokensRequest * @generated from message memos.api.v1.ListPersonalAccessTokensRequest
...@@ -868,7 +1033,7 @@ export type ListPersonalAccessTokensRequest = Message<"memos.api.v1.ListPersonal ...@@ -868,7 +1033,7 @@ export type ListPersonalAccessTokensRequest = Message<"memos.api.v1.ListPersonal
* Use `create(ListPersonalAccessTokensRequestSchema)` to create a new message. * Use `create(ListPersonalAccessTokensRequestSchema)` to create a new message.
*/ */
export const ListPersonalAccessTokensRequestSchema: GenMessage<ListPersonalAccessTokensRequest> = /*@__PURE__*/ export const ListPersonalAccessTokensRequestSchema: GenMessage<ListPersonalAccessTokensRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 19); messageDesc(file_api_v1_user_service, 25);
/** /**
* @generated from message memos.api.v1.ListPersonalAccessTokensResponse * @generated from message memos.api.v1.ListPersonalAccessTokensResponse
...@@ -901,7 +1066,7 @@ export type ListPersonalAccessTokensResponse = Message<"memos.api.v1.ListPersona ...@@ -901,7 +1066,7 @@ export type ListPersonalAccessTokensResponse = Message<"memos.api.v1.ListPersona
* Use `create(ListPersonalAccessTokensResponseSchema)` to create a new message. * Use `create(ListPersonalAccessTokensResponseSchema)` to create a new message.
*/ */
export const ListPersonalAccessTokensResponseSchema: GenMessage<ListPersonalAccessTokensResponse> = /*@__PURE__*/ export const ListPersonalAccessTokensResponseSchema: GenMessage<ListPersonalAccessTokensResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 20); messageDesc(file_api_v1_user_service, 26);
/** /**
* @generated from message memos.api.v1.CreatePersonalAccessTokenRequest * @generated from message memos.api.v1.CreatePersonalAccessTokenRequest
...@@ -935,7 +1100,7 @@ export type CreatePersonalAccessTokenRequest = Message<"memos.api.v1.CreatePerso ...@@ -935,7 +1100,7 @@ export type CreatePersonalAccessTokenRequest = Message<"memos.api.v1.CreatePerso
* Use `create(CreatePersonalAccessTokenRequestSchema)` to create a new message. * Use `create(CreatePersonalAccessTokenRequestSchema)` to create a new message.
*/ */
export const CreatePersonalAccessTokenRequestSchema: GenMessage<CreatePersonalAccessTokenRequest> = /*@__PURE__*/ export const CreatePersonalAccessTokenRequestSchema: GenMessage<CreatePersonalAccessTokenRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 21); messageDesc(file_api_v1_user_service, 27);
/** /**
* @generated from message memos.api.v1.CreatePersonalAccessTokenResponse * @generated from message memos.api.v1.CreatePersonalAccessTokenResponse
...@@ -962,7 +1127,7 @@ export type CreatePersonalAccessTokenResponse = Message<"memos.api.v1.CreatePers ...@@ -962,7 +1127,7 @@ export type CreatePersonalAccessTokenResponse = Message<"memos.api.v1.CreatePers
* Use `create(CreatePersonalAccessTokenResponseSchema)` to create a new message. * Use `create(CreatePersonalAccessTokenResponseSchema)` to create a new message.
*/ */
export const CreatePersonalAccessTokenResponseSchema: GenMessage<CreatePersonalAccessTokenResponse> = /*@__PURE__*/ export const CreatePersonalAccessTokenResponseSchema: GenMessage<CreatePersonalAccessTokenResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 22); messageDesc(file_api_v1_user_service, 28);
/** /**
* @generated from message memos.api.v1.DeletePersonalAccessTokenRequest * @generated from message memos.api.v1.DeletePersonalAccessTokenRequest
...@@ -982,7 +1147,7 @@ export type DeletePersonalAccessTokenRequest = Message<"memos.api.v1.DeletePerso ...@@ -982,7 +1147,7 @@ export type DeletePersonalAccessTokenRequest = Message<"memos.api.v1.DeletePerso
* Use `create(DeletePersonalAccessTokenRequestSchema)` to create a new message. * Use `create(DeletePersonalAccessTokenRequestSchema)` to create a new message.
*/ */
export const DeletePersonalAccessTokenRequestSchema: GenMessage<DeletePersonalAccessTokenRequest> = /*@__PURE__*/ export const DeletePersonalAccessTokenRequestSchema: GenMessage<DeletePersonalAccessTokenRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 23); messageDesc(file_api_v1_user_service, 29);
/** /**
* UserWebhook represents a webhook owned by a user. * UserWebhook represents a webhook owned by a user.
...@@ -1032,7 +1197,7 @@ export type UserWebhook = Message<"memos.api.v1.UserWebhook"> & { ...@@ -1032,7 +1197,7 @@ export type UserWebhook = Message<"memos.api.v1.UserWebhook"> & {
* Use `create(UserWebhookSchema)` to create a new message. * Use `create(UserWebhookSchema)` to create a new message.
*/ */
export const UserWebhookSchema: GenMessage<UserWebhook> = /*@__PURE__*/ export const UserWebhookSchema: GenMessage<UserWebhook> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 24); messageDesc(file_api_v1_user_service, 30);
/** /**
* @generated from message memos.api.v1.ListUserWebhooksRequest * @generated from message memos.api.v1.ListUserWebhooksRequest
...@@ -1052,7 +1217,7 @@ export type ListUserWebhooksRequest = Message<"memos.api.v1.ListUserWebhooksRequ ...@@ -1052,7 +1217,7 @@ export type ListUserWebhooksRequest = Message<"memos.api.v1.ListUserWebhooksRequ
* Use `create(ListUserWebhooksRequestSchema)` to create a new message. * Use `create(ListUserWebhooksRequestSchema)` to create a new message.
*/ */
export const ListUserWebhooksRequestSchema: GenMessage<ListUserWebhooksRequest> = /*@__PURE__*/ export const ListUserWebhooksRequestSchema: GenMessage<ListUserWebhooksRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 25); messageDesc(file_api_v1_user_service, 31);
/** /**
* @generated from message memos.api.v1.ListUserWebhooksResponse * @generated from message memos.api.v1.ListUserWebhooksResponse
...@@ -1071,7 +1236,7 @@ export type ListUserWebhooksResponse = Message<"memos.api.v1.ListUserWebhooksRes ...@@ -1071,7 +1236,7 @@ export type ListUserWebhooksResponse = Message<"memos.api.v1.ListUserWebhooksRes
* Use `create(ListUserWebhooksResponseSchema)` to create a new message. * Use `create(ListUserWebhooksResponseSchema)` to create a new message.
*/ */
export const ListUserWebhooksResponseSchema: GenMessage<ListUserWebhooksResponse> = /*@__PURE__*/ export const ListUserWebhooksResponseSchema: GenMessage<ListUserWebhooksResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 26); messageDesc(file_api_v1_user_service, 32);
/** /**
* @generated from message memos.api.v1.CreateUserWebhookRequest * @generated from message memos.api.v1.CreateUserWebhookRequest
...@@ -1098,7 +1263,7 @@ export type CreateUserWebhookRequest = Message<"memos.api.v1.CreateUserWebhookRe ...@@ -1098,7 +1263,7 @@ export type CreateUserWebhookRequest = Message<"memos.api.v1.CreateUserWebhookRe
* Use `create(CreateUserWebhookRequestSchema)` to create a new message. * Use `create(CreateUserWebhookRequestSchema)` to create a new message.
*/ */
export const CreateUserWebhookRequestSchema: GenMessage<CreateUserWebhookRequest> = /*@__PURE__*/ export const CreateUserWebhookRequestSchema: GenMessage<CreateUserWebhookRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 27); messageDesc(file_api_v1_user_service, 33);
/** /**
* @generated from message memos.api.v1.UpdateUserWebhookRequest * @generated from message memos.api.v1.UpdateUserWebhookRequest
...@@ -1124,7 +1289,7 @@ export type UpdateUserWebhookRequest = Message<"memos.api.v1.UpdateUserWebhookRe ...@@ -1124,7 +1289,7 @@ export type UpdateUserWebhookRequest = Message<"memos.api.v1.UpdateUserWebhookRe
* Use `create(UpdateUserWebhookRequestSchema)` to create a new message. * Use `create(UpdateUserWebhookRequestSchema)` to create a new message.
*/ */
export const UpdateUserWebhookRequestSchema: GenMessage<UpdateUserWebhookRequest> = /*@__PURE__*/ export const UpdateUserWebhookRequestSchema: GenMessage<UpdateUserWebhookRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 28); messageDesc(file_api_v1_user_service, 34);
/** /**
* @generated from message memos.api.v1.DeleteUserWebhookRequest * @generated from message memos.api.v1.DeleteUserWebhookRequest
...@@ -1144,7 +1309,7 @@ export type DeleteUserWebhookRequest = Message<"memos.api.v1.DeleteUserWebhookRe ...@@ -1144,7 +1309,7 @@ export type DeleteUserWebhookRequest = Message<"memos.api.v1.DeleteUserWebhookRe
* Use `create(DeleteUserWebhookRequestSchema)` to create a new message. * Use `create(DeleteUserWebhookRequestSchema)` to create a new message.
*/ */
export const DeleteUserWebhookRequestSchema: GenMessage<DeleteUserWebhookRequest> = /*@__PURE__*/ export const DeleteUserWebhookRequestSchema: GenMessage<DeleteUserWebhookRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 29); messageDesc(file_api_v1_user_service, 35);
/** /**
* @generated from message memos.api.v1.UserNotification * @generated from message memos.api.v1.UserNotification
...@@ -1217,7 +1382,7 @@ export type UserNotification = Message<"memos.api.v1.UserNotification"> & { ...@@ -1217,7 +1382,7 @@ export type UserNotification = Message<"memos.api.v1.UserNotification"> & {
* Use `create(UserNotificationSchema)` to create a new message. * Use `create(UserNotificationSchema)` to create a new message.
*/ */
export const UserNotificationSchema: GenMessage<UserNotification> = /*@__PURE__*/ export const UserNotificationSchema: GenMessage<UserNotification> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 30); messageDesc(file_api_v1_user_service, 36);
/** /**
* @generated from message memos.api.v1.UserNotification.MemoCommentPayload * @generated from message memos.api.v1.UserNotification.MemoCommentPayload
...@@ -1259,7 +1424,7 @@ export type UserNotification_MemoCommentPayload = Message<"memos.api.v1.UserNoti ...@@ -1259,7 +1424,7 @@ export type UserNotification_MemoCommentPayload = Message<"memos.api.v1.UserNoti
* Use `create(UserNotification_MemoCommentPayloadSchema)` to create a new message. * Use `create(UserNotification_MemoCommentPayloadSchema)` to create a new message.
*/ */
export const UserNotification_MemoCommentPayloadSchema: GenMessage<UserNotification_MemoCommentPayload> = /*@__PURE__*/ export const UserNotification_MemoCommentPayloadSchema: GenMessage<UserNotification_MemoCommentPayload> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 30, 0); messageDesc(file_api_v1_user_service, 36, 0);
/** /**
* @generated from message memos.api.v1.UserNotification.MemoMentionPayload * @generated from message memos.api.v1.UserNotification.MemoMentionPayload
...@@ -1301,7 +1466,7 @@ export type UserNotification_MemoMentionPayload = Message<"memos.api.v1.UserNoti ...@@ -1301,7 +1466,7 @@ export type UserNotification_MemoMentionPayload = Message<"memos.api.v1.UserNoti
* Use `create(UserNotification_MemoMentionPayloadSchema)` to create a new message. * Use `create(UserNotification_MemoMentionPayloadSchema)` to create a new message.
*/ */
export const UserNotification_MemoMentionPayloadSchema: GenMessage<UserNotification_MemoMentionPayload> = /*@__PURE__*/ export const UserNotification_MemoMentionPayloadSchema: GenMessage<UserNotification_MemoMentionPayload> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 30, 1); messageDesc(file_api_v1_user_service, 36, 1);
/** /**
* @generated from enum memos.api.v1.UserNotification.Status * @generated from enum memos.api.v1.UserNotification.Status
...@@ -1327,7 +1492,7 @@ export enum UserNotification_Status { ...@@ -1327,7 +1492,7 @@ export enum UserNotification_Status {
* Describes the enum memos.api.v1.UserNotification.Status. * Describes the enum memos.api.v1.UserNotification.Status.
*/ */
export const UserNotification_StatusSchema: GenEnum<UserNotification_Status> = /*@__PURE__*/ export const UserNotification_StatusSchema: GenEnum<UserNotification_Status> = /*@__PURE__*/
enumDesc(file_api_v1_user_service, 30, 0); enumDesc(file_api_v1_user_service, 36, 0);
/** /**
* @generated from enum memos.api.v1.UserNotification.Type * @generated from enum memos.api.v1.UserNotification.Type
...@@ -1353,7 +1518,7 @@ export enum UserNotification_Type { ...@@ -1353,7 +1518,7 @@ export enum UserNotification_Type {
* Describes the enum memos.api.v1.UserNotification.Type. * Describes the enum memos.api.v1.UserNotification.Type.
*/ */
export const UserNotification_TypeSchema: GenEnum<UserNotification_Type> = /*@__PURE__*/ export const UserNotification_TypeSchema: GenEnum<UserNotification_Type> = /*@__PURE__*/
enumDesc(file_api_v1_user_service, 30, 1); enumDesc(file_api_v1_user_service, 36, 1);
/** /**
* @generated from message memos.api.v1.ListUserNotificationsRequest * @generated from message memos.api.v1.ListUserNotificationsRequest
...@@ -1388,7 +1553,7 @@ export type ListUserNotificationsRequest = Message<"memos.api.v1.ListUserNotific ...@@ -1388,7 +1553,7 @@ export type ListUserNotificationsRequest = Message<"memos.api.v1.ListUserNotific
* Use `create(ListUserNotificationsRequestSchema)` to create a new message. * Use `create(ListUserNotificationsRequestSchema)` to create a new message.
*/ */
export const ListUserNotificationsRequestSchema: GenMessage<ListUserNotificationsRequest> = /*@__PURE__*/ export const ListUserNotificationsRequestSchema: GenMessage<ListUserNotificationsRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 31); messageDesc(file_api_v1_user_service, 37);
/** /**
* @generated from message memos.api.v1.ListUserNotificationsResponse * @generated from message memos.api.v1.ListUserNotificationsResponse
...@@ -1410,7 +1575,7 @@ export type ListUserNotificationsResponse = Message<"memos.api.v1.ListUserNotifi ...@@ -1410,7 +1575,7 @@ export type ListUserNotificationsResponse = Message<"memos.api.v1.ListUserNotifi
* Use `create(ListUserNotificationsResponseSchema)` to create a new message. * Use `create(ListUserNotificationsResponseSchema)` to create a new message.
*/ */
export const ListUserNotificationsResponseSchema: GenMessage<ListUserNotificationsResponse> = /*@__PURE__*/ export const ListUserNotificationsResponseSchema: GenMessage<ListUserNotificationsResponse> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 32); messageDesc(file_api_v1_user_service, 38);
/** /**
* @generated from message memos.api.v1.UpdateUserNotificationRequest * @generated from message memos.api.v1.UpdateUserNotificationRequest
...@@ -1432,7 +1597,7 @@ export type UpdateUserNotificationRequest = Message<"memos.api.v1.UpdateUserNoti ...@@ -1432,7 +1597,7 @@ export type UpdateUserNotificationRequest = Message<"memos.api.v1.UpdateUserNoti
* Use `create(UpdateUserNotificationRequestSchema)` to create a new message. * Use `create(UpdateUserNotificationRequestSchema)` to create a new message.
*/ */
export const UpdateUserNotificationRequestSchema: GenMessage<UpdateUserNotificationRequest> = /*@__PURE__*/ export const UpdateUserNotificationRequestSchema: GenMessage<UpdateUserNotificationRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 33); messageDesc(file_api_v1_user_service, 39);
/** /**
* @generated from message memos.api.v1.DeleteUserNotificationRequest * @generated from message memos.api.v1.DeleteUserNotificationRequest
...@@ -1451,7 +1616,7 @@ export type DeleteUserNotificationRequest = Message<"memos.api.v1.DeleteUserNoti ...@@ -1451,7 +1616,7 @@ export type DeleteUserNotificationRequest = Message<"memos.api.v1.DeleteUserNoti
* Use `create(DeleteUserNotificationRequestSchema)` to create a new message. * Use `create(DeleteUserNotificationRequestSchema)` to create a new message.
*/ */
export const DeleteUserNotificationRequestSchema: GenMessage<DeleteUserNotificationRequest> = /*@__PURE__*/ export const DeleteUserNotificationRequestSchema: GenMessage<DeleteUserNotificationRequest> = /*@__PURE__*/
messageDesc(file_api_v1_user_service, 34); messageDesc(file_api_v1_user_service, 40);
/** /**
* @generated from service memos.api.v1.UserService * @generated from service memos.api.v1.UserService
...@@ -1568,6 +1733,46 @@ export const UserService: GenService<{ ...@@ -1568,6 +1733,46 @@ export const UserService: GenService<{
input: typeof ListUserSettingsRequestSchema; input: typeof ListUserSettingsRequestSchema;
output: typeof ListUserSettingsResponseSchema; output: typeof ListUserSettingsResponseSchema;
}, },
/**
* ListLinkedIdentities returns a list of linked SSO identities for a user.
*
* @generated from rpc memos.api.v1.UserService.ListLinkedIdentities
*/
listLinkedIdentities: {
methodKind: "unary";
input: typeof ListLinkedIdentitiesRequestSchema;
output: typeof ListLinkedIdentitiesResponseSchema;
},
/**
* CreateLinkedIdentity links an SSO identity to the authenticated user.
*
* @generated from rpc memos.api.v1.UserService.CreateLinkedIdentity
*/
createLinkedIdentity: {
methodKind: "unary";
input: typeof CreateLinkedIdentityRequestSchema;
output: typeof LinkedIdentitySchema;
},
/**
* GetLinkedIdentity gets a linked SSO identity for a user.
*
* @generated from rpc memos.api.v1.UserService.GetLinkedIdentity
*/
getLinkedIdentity: {
methodKind: "unary";
input: typeof GetLinkedIdentityRequestSchema;
output: typeof LinkedIdentitySchema;
},
/**
* DeleteLinkedIdentity unlinks an SSO identity from a user.
*
* @generated from rpc memos.api.v1.UserService.DeleteLinkedIdentity
*/
deleteLinkedIdentity: {
methodKind: "unary";
input: typeof DeleteLinkedIdentityRequestSchema;
output: typeof EmptySchema;
},
/** /**
* ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user. * ListPersonalAccessTokens returns a list of Personal Access Tokens (PATs) for a user.
* PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens. * PATs are long-lived tokens for API/script access, distinct from short-lived JWT access tokens.
......
const STATE_STORAGE_KEY = "oauth_state"; const STATE_STORAGE_KEY = "oauth_state";
const STATE_EXPIRY_MS = 10 * 60 * 1000; // 10 minutes const STATE_EXPIRY_MS = 10 * 60 * 1000; // 10 minutes
export type OAuthFlowMode = "signin" | "link";
interface OAuthState { interface OAuthState {
state: string; state: string;
identityProviderName: string; identityProviderName: string;
flowMode: OAuthFlowMode;
timestamp: number; timestamp: number;
returnUrl?: string; returnUrl?: string;
linkingUserName?: string;
codeVerifier?: string; // PKCE code_verifier codeVerifier?: string; // PKCE code_verifier
} }
...@@ -44,7 +48,9 @@ function base64UrlEncode(buffer: Uint8Array): string { ...@@ -44,7 +48,9 @@ function base64UrlEncode(buffer: Uint8Array): string {
// PKCE is optional - if crypto APIs are unavailable (HTTP context), falls back to standard OAuth // PKCE is optional - if crypto APIs are unavailable (HTTP context), falls back to standard OAuth
export async function storeOAuthState( export async function storeOAuthState(
identityProviderName: string, identityProviderName: string,
flowMode: OAuthFlowMode,
returnUrl?: string, returnUrl?: string,
linkingUserName?: string,
): Promise<{ state: string; codeChallenge?: string }> { ): Promise<{ state: string; codeChallenge?: string }> {
const state = generateSecureState(); const state = generateSecureState();
...@@ -74,8 +80,10 @@ export async function storeOAuthState( ...@@ -74,8 +80,10 @@ export async function storeOAuthState(
const stateData: OAuthState = { const stateData: OAuthState = {
state, state,
identityProviderName, identityProviderName,
flowMode,
timestamp: Date.now(), timestamp: Date.now(),
returnUrl, returnUrl,
linkingUserName,
codeVerifier, // Store for later retrieval in callback (undefined if PKCE not available) codeVerifier, // Store for later retrieval in callback (undefined if PKCE not available)
}; };
...@@ -90,8 +98,10 @@ export async function storeOAuthState( ...@@ -90,8 +98,10 @@ export async function storeOAuthState(
} }
// Validate and retrieve OAuth state from storage (CSRF protection) // Validate and retrieve OAuth state from storage (CSRF protection)
// Returns identityProviderName, returnUrl, and codeVerifier for PKCE // Returns identityProviderName, flowMode, returnUrl, linkingUserName, and codeVerifier for PKCE
export function validateOAuthState(stateParam: string): { identityProviderName: string; returnUrl?: string; codeVerifier?: string } | null { export function validateOAuthState(
stateParam: string,
): { identityProviderName: string; flowMode: OAuthFlowMode; returnUrl?: string; linkingUserName?: string; codeVerifier?: string } | null {
try { try {
const storedData = sessionStorage.getItem(STATE_STORAGE_KEY); const storedData = sessionStorage.getItem(STATE_STORAGE_KEY);
if (!storedData) { if (!storedData) {
...@@ -119,7 +129,9 @@ export function validateOAuthState(stateParam: string): { identityProviderName: ...@@ -119,7 +129,9 @@ export function validateOAuthState(stateParam: string): { identityProviderName:
sessionStorage.removeItem(STATE_STORAGE_KEY); sessionStorage.removeItem(STATE_STORAGE_KEY);
return { return {
identityProviderName: stateData.identityProviderName, identityProviderName: stateData.identityProviderName,
flowMode: stateData.flowMode || "signin",
returnUrl: stateData.returnUrl, returnUrl: stateData.returnUrl,
linkingUserName: stateData.linkingUserName,
codeVerifier: stateData.codeVerifier, // Return PKCE code_verifier codeVerifier: stateData.codeVerifier, // Return PKCE code_verifier
}; };
} catch (error) { } catch (error) {
......
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { storeOAuthState, validateOAuthState } from "@/utils/oauth";
describe("oauth state", () => {
beforeEach(() => {
sessionStorage.clear();
});
afterEach(() => {
sessionStorage.clear();
});
it("round-trips the linking user for link flows", async () => {
const { state } = await storeOAuthState("identity-providers/google", "link", "/settings", "users/alice");
expect(validateOAuthState(state)).toEqual({
identityProviderName: "identity-providers/google",
flowMode: "link",
returnUrl: "/settings",
linkingUserName: "users/alice",
codeVerifier: expect.any(String),
});
});
it("defaults older states to signin without a linking user", () => {
sessionStorage.setItem(
"oauth_state",
JSON.stringify({
state: "legacy-state",
identityProviderName: "identity-providers/google",
timestamp: Date.now(),
returnUrl: "/auth",
}),
);
expect(validateOAuthState("legacy-state")).toEqual({
identityProviderName: "identity-providers/google",
flowMode: "signin",
returnUrl: "/auth",
linkingUserName: undefined,
codeVerifier: undefined,
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment