Skip to content

Commit d9de6db

Browse files
committed
rewrite admin password reset
1 parent 0fc9693 commit d9de6db

File tree

11 files changed

+52
-247
lines changed

11 files changed

+52
-247
lines changed

src/packages/conat/hub-api/db.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { authFirst } from "./util";
1+
import { authFirst, requireAccount } from "./util";
22

33
export const db = {
44
userQuery: authFirst,
55
touch: authFirst,
66
getLegacyTimeTravelInfo: authFirst,
77
getLegacyTimeTravelPatches: authFirst,
88
fileUseTimes: authFirst,
9-
removeBlobTtls: authFirst,
9+
removeBlobTtls: requireAccount,
1010
};
1111

1212
export interface DB {

src/packages/conat/hub-api/system.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const system = {
1616
revokeUserAuthToken: noAuth,
1717
userSearch: authFirst,
1818
getNames: requireAccount,
19+
adminResetPasswordLink: authFirst,
1920
};
2021

2122
export interface System {
@@ -68,4 +69,17 @@ export interface System {
6869
}
6970
| undefined;
7071
}>;
72+
73+
// adminResetPasswordLink: Enables admins (and only admins!) to generate and get a password reset
74+
// for another user. The response message contains a password reset link,
75+
// though without the site part of the url (the client should fill that in).
76+
// This makes it possible for admins to reset passwords of users, even if
77+
// sending email is not setup, e.g., for cocalc-docker, and also deals with the
78+
// possibility that users have no email address, or broken email, or they
79+
// can't receive email due to crazy spam filtering.
80+
// Non-admins always get back an error.
81+
adminResetPasswordLink: (opts: {
82+
account_id?: string;
83+
user_account_id: string;
84+
}) => Promise<string>;
7185
}

src/packages/frontend/admin/users/password-reset.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@
55

66
import { Component, Rendered } from "@cocalc/frontend/app-framework";
77
import { Button } from "@cocalc/frontend/antd-bootstrap";
8-
import { CopyToClipBoard, Icon, ErrorDisplay } from "@cocalc/frontend/components";
8+
import {
9+
CopyToClipBoard,
10+
Icon,
11+
ErrorDisplay,
12+
} from "@cocalc/frontend/components";
913
import { webapp_client } from "../../webapp-client";
1014
import { appBasePath } from "@cocalc/frontend/customize/app-base-path";
1115

1216
interface Props {
13-
email_address?: string;
17+
account_id: string;
18+
email_address: string;
1419
}
1520

1621
interface State {
@@ -32,12 +37,11 @@ export class PasswordReset extends Component<Props, State> {
3237
}
3338

3439
async do_request(): Promise<void> {
35-
if (!this.props.email_address) throw Error("bug");
3640
this.setState({ running: true });
3741
let link: string;
3842
try {
39-
link = await webapp_client.admin_client.admin_reset_password(
40-
this.props.email_address
43+
link = await webapp_client.conat_client.hub.system.adminResetPasswordLink(
44+
{ user_account_id: this.props.account_id },
4145
);
4246
} catch (err) {
4347
if (!this.mounted) return;
@@ -74,6 +78,7 @@ export class PasswordReset extends Component<Props, State> {
7478
}
7579
return (
7680
<ErrorDisplay
81+
style={{ margin: "30px" }}
7782
error={this.state.error}
7883
onClose={() => {
7984
this.setState({ error: undefined });

src/packages/frontend/admin/users/user.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,12 @@ export function UserResult({
155155
last_name={last_name ?? ""}
156156
/>
157157
)}
158-
{state.password && (
158+
{state.password && email_address && (
159159
<Card title="Password">
160-
<PasswordReset email_address={email_address} />
160+
<PasswordReset
161+
account_id={account_id}
162+
email_address={email_address}
163+
/>
161164
</Card>
162165
)}
163166
{state.ban && (

src/packages/frontend/client/admin.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,6 @@ export class AdminClient {
1414
this.client = client;
1515
}
1616

17-
public async admin_reset_password(email_address: string): Promise<string> {
18-
return (
19-
await this.client.async_call({
20-
message: message.admin_reset_password({
21-
email_address,
22-
}),
23-
allow_post: true,
24-
})
25-
).link;
26-
}
27-
2817
public async admin_ban_user(
2918
account_id: string,
3019
ban: boolean = true, // if true, ban user -- if false, remove ban

src/packages/frontend/client/llm.ts

Lines changed: 1 addition & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,14 @@
55

66
import { delay } from "awaiting";
77
import { EventEmitter } from "events";
8-
98
import { redux } from "@cocalc/frontend/app-framework";
10-
import type { EmbeddingData } from "@cocalc/util/db-schema/llm";
11-
import {
12-
MAX_EMBEDDINGS_TOKENS,
13-
MAX_REMOVE_LIMIT,
14-
MAX_SAVE_LIMIT,
15-
MAX_SEARCH_LIMIT,
16-
} from "@cocalc/util/db-schema/llm";
179
import {
1810
LanguageModel,
1911
LanguageServiceCore,
2012
getSystemPrompt,
2113
isFreeModel,
2214
model2service,
2315
} from "@cocalc/util/db-schema/llm-utils";
24-
import * as message from "@cocalc/util/message";
2516
import type { WebappClient } from "./client";
2617
import type { History } from "./types";
2718
import {
@@ -43,15 +34,6 @@ interface QueryLLMProps {
4334
startStreamExplicitly?: boolean;
4435
}
4536

46-
interface EmbeddingsQuery {
47-
scope: string | string[];
48-
limit: number; // client automatically deals with large limit by making multiple requests (i.e., there is no limit on the limit)
49-
text?: string;
50-
filter?: object;
51-
selector?: { include?: string[]; exclude?: string[] };
52-
offset?: number | string;
53-
}
54-
5537
export class LLMClient {
5638
private client: WebappClient;
5739

@@ -200,147 +182,14 @@ export class LLMClient {
200182

201183
return "see stream for output";
202184
}
203-
204-
public async embeddings_search(
205-
query: EmbeddingsQuery,
206-
): Promise<{ id: string; payload: object }[]> {
207-
let limit = Math.min(MAX_SEARCH_LIMIT, query.limit);
208-
const result = await this.embeddings_search_call({ ...query, limit });
209-
210-
if (result.length >= MAX_SEARCH_LIMIT) {
211-
// get additional pages
212-
while (true) {
213-
const offset =
214-
query.text == null ? result[result.length - 1].id : result.length;
215-
const page = await this.embeddings_search_call({
216-
...query,
217-
limit,
218-
offset,
219-
});
220-
// Include the new elements
221-
result.push(...page);
222-
if (page.length < MAX_SEARCH_LIMIT) {
223-
// didn't reach the limit, so we're done.
224-
break;
225-
}
226-
}
227-
}
228-
return result;
229-
}
230-
231-
private async embeddings_search_call({
232-
scope,
233-
limit,
234-
text,
235-
filter,
236-
selector,
237-
offset,
238-
}: EmbeddingsQuery) {
239-
text = text?.trim();
240-
const resp = await this.client.async_call({
241-
message: message.openai_embeddings_search({
242-
scope,
243-
text,
244-
filter,
245-
limit,
246-
selector,
247-
offset,
248-
}),
249-
});
250-
return resp.matches;
251-
}
252-
253-
public async embeddings_save({
254-
project_id,
255-
path,
256-
data: data0,
257-
}: {
258-
project_id: string;
259-
path: string;
260-
data: EmbeddingData[];
261-
}): Promise<string[]> {
262-
this.assertHasNeuralSearch();
263-
const { truncateMessage } = await import("@cocalc/frontend/misc/llm");
264-
265-
// Make data be data0, but without mutate data0
266-
// and with any text truncated to fit in the
267-
// embeddings limit.
268-
const data: EmbeddingData[] = [];
269-
for (const x of data0) {
270-
const { text } = x;
271-
if (typeof text != "string") {
272-
throw Error("text must be a string");
273-
}
274-
const text1 = truncateMessage(text, MAX_EMBEDDINGS_TOKENS);
275-
if (text1.length != text.length) {
276-
data.push({ ...x, text: text1 });
277-
} else {
278-
data.push(x);
279-
}
280-
}
281-
282-
const ids: string[] = [];
283-
let v = data;
284-
while (v.length > 0) {
285-
const resp = await this.client.async_call({
286-
message: message.openai_embeddings_save({
287-
project_id,
288-
path,
289-
data: v.slice(0, MAX_SAVE_LIMIT),
290-
}),
291-
});
292-
ids.push(...resp.ids);
293-
v = v.slice(MAX_SAVE_LIMIT);
294-
}
295-
296-
return ids;
297-
}
298-
299-
public async embeddings_remove({
300-
project_id,
301-
path,
302-
data,
303-
}: {
304-
project_id: string;
305-
path: string;
306-
data: EmbeddingData[];
307-
}): Promise<string[]> {
308-
this.assertHasNeuralSearch();
309-
310-
const ids: string[] = [];
311-
let v = data;
312-
while (v.length > 0) {
313-
const resp = await this.client.async_call({
314-
message: message.openai_embeddings_remove({
315-
project_id,
316-
path,
317-
data: v.slice(0, MAX_REMOVE_LIMIT),
318-
}),
319-
});
320-
ids.push(...resp.ids);
321-
v = v.slice(MAX_REMOVE_LIMIT);
322-
}
323-
324-
return ids;
325-
}
326-
327-
neuralSearchIsEnabled(): boolean {
328-
return !!redux.getStore("customize").get("neural_search_enabled");
329-
}
330-
331-
assertHasNeuralSearch() {
332-
if (!this.neuralSearchIsEnabled()) {
333-
throw Error("OpenAI support is not currently enabled on this server");
334-
}
335-
}
336185
}
337186

338187
class ChatStream extends EventEmitter {
339188
constructor() {
340189
super();
341190
}
342191

343-
process = (text: string|null) => {
192+
process = (text: string | null) => {
344193
// emits undefined text when done (or err below)
345194
this.emit("token", text);
346195
};

src/packages/frontend/project_actions.ts

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3042,38 +3042,6 @@ export class ProjectActions extends Actions<ProjectStoreState> {
30423042
}
30433043
}
30443044

3045-
private async neuralSearch(text, path) {
3046-
try {
3047-
const scope = `projects/${this.project_id}/files/${path}`;
3048-
const results = await webapp_client.openai_client.embeddings_search({
3049-
text,
3050-
limit: 25,
3051-
scope,
3052-
});
3053-
const search_results: {
3054-
filename: string;
3055-
description: string;
3056-
fragment_id?: FragmentId;
3057-
}[] = [];
3058-
for (const result of results) {
3059-
const url = result.payload["url"] as string | undefined;
3060-
if (!url) continue;
3061-
const [filename, fragment_id] = url.slice(scope.length + 1).split("#");
3062-
const description = result.payload["text"] ?? "";
3063-
search_results.push({
3064-
filename: filename[0] == "/" ? filename.slice(1) : filename,
3065-
description,
3066-
fragment_id: Fragment.decode(fragment_id),
3067-
});
3068-
}
3069-
this.setState({ search_results });
3070-
} catch (err) {
3071-
this.setState({
3072-
search_error: `${err}`,
3073-
});
3074-
}
3075-
}
3076-
30773045
search = () => {
30783046
let cmd, ins;
30793047
const store = this.get_store();
@@ -3105,10 +3073,6 @@ export class ProjectActions extends Actions<ProjectStoreState> {
31053073
git_grep: store.get("git_grep"),
31063074
});
31073075

3108-
if (store.get("neural_search")) {
3109-
this.neuralSearch(query, path);
3110-
return;
3111-
}
31123076

31133077
// generate the grep command for the given query with the given flags
31143078
if (store.get("case_sensitive")) {

src/packages/hub/client.coffee

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1477,28 +1477,6 @@ class exports.Client extends EventEmitter
14771477
@push_to_client(message.success(id:mesg.id))
14781478

14791479

1480-
mesg_admin_reset_password: (mesg) =>
1481-
dbg = @dbg("mesg_reset_password")
1482-
dbg(mesg.email_address)
1483-
try
1484-
if not misc.is_valid_email_address(mesg.email_address)
1485-
throw Error("invalid email address")
1486-
await callback(@assert_user_is_in_group, 'admin')
1487-
if not await callback2(@database.account_exists, {email_address : mesg.email_address})
1488-
throw Error("no such account with email #{mesg.email_address}")
1489-
# We now know that there is an account with this email address.
1490-
# put entry in the password_reset uuid:value table with ttl of 24 hours.
1491-
# NOTE: when users request their own reset, the ttl is 1 hour, but when we
1492-
# as admins send one manually, they typically need more time, so 1 day instead.
1493-
# We used 8 hours for a while and it is often not enough time.
1494-
id = await callback2(@database.set_password_reset, {email_address : mesg.email_address, ttl:24*60*60});
1495-
mesg.link = "/auth/password-reset/#{id}"
1496-
@push_to_client(mesg)
1497-
catch err
1498-
dbg("failed -- #{err}")
1499-
@error_to_client(id:mesg.id, error:"#{err}")
1500-
1501-
15021480
# These are deprecated. Not the best approach.
15031481
mesg_openai_embeddings_search: (mesg) =>
15041482
@error_to_client(id:mesg.id, error:"openai_embeddings_search is DEPRECATED")

0 commit comments

Comments
 (0)