···2import { BaseLayout } from "../layout/main";
3import NavTags from "../layout/navTags";
4import AccountHandler from "../layout/account";
5-import UsernameField from "../layout/usernameField";
6import TurnstileCaptcha from "../layout/turnstile";
78export default function ForgotPassword(props:any) {
···2import { BaseLayout } from "../layout/main";
3import NavTags from "../layout/navTags";
4import AccountHandler from "../layout/account";
5+import { UsernameField } from "../layout/usernameField";
6import TurnstileCaptcha from "../layout/turnstile";
78export default function ForgotPassword(props:any) {
+4-2
src/pages/login.tsx
···1import { BaseLayout } from "../layout/main";
2import NavTags from "../layout/navTags";
3import AccountHandler from "../layout/account";
4-import UsernameField from "../layout/usernameField";
0056export default function Login() {
7 const links = [{title: "Sign Up", url: "/signup"}, {title: "Forgot Password", url: "/forgot"}];
···1819 <label>
20 Dashboard Password
21- <input type="password" name="password" id="password" required />
22 <small><b>NOTE</b>: This password is not related to your bluesky account!</small>
23 </label>
24 </AccountHandler>
···1import { BaseLayout } from "../layout/main";
2import NavTags from "../layout/navTags";
3import AccountHandler from "../layout/account";
4+import { UsernameField } from "../layout/usernameField";
5+import { DashboardPasswordField } from "../layout/passwordFields";
6+import { PWAutoCompleteSettings } from "../types.d";
78export default function Login() {
9 const links = [{title: "Sign Up", url: "/signup"}, {title: "Forgot Password", url: "/forgot"}];
···2021 <label>
22 Dashboard Password
23+ <DashboardPasswordField autocomplete={PWAutoCompleteSettings.CurrentPass} required={true} />
24 <small><b>NOTE</b>: This password is not related to your bluesky account!</small>
25 </label>
26 </AccountHandler>
+6-6
src/pages/signup.tsx
···1import { Context } from "hono";
2import { BaseLayout } from "../layout/main";
3import { isUsingInviteKeys } from "../utils/inviteKeys";
4-import { BSKY_MAX_APP_PASSWORD_LENGTH, MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits.d";
5import NavTags from "../layout/navTags";
6import isEmpty from "just-is-empty";
7import AccountHandler from "../layout/account";
8-import UsernameField from "../layout/usernameField";
9import TurnstileCaptcha from "../layout/turnstile";
10import FooterCopyright from "../layout/footer";
001112export default function Signup(props:any) {
13 const ctx: Context = props.c;
···3031 <label>
32 Dashboard Password
33- <input type="password" name="password" minlength={MIN_DASHBOARD_PASS} maxlength={MAX_DASHBOARD_PASS} required
34- autocomplete="new-password" />
35 <small>Create a new password to use to login to this website. Passwords should be {MIN_DASHBOARD_PASS} to {MAX_DASHBOARD_PASS} characters long.</small>
36 </label>
3738 <label>
39 Bluesky App Password
40- <input type="password" name="bskyAppPassword" maxlength={BSKY_MAX_APP_PASSWORD_LENGTH} placeholder="" required
41- data-1p-ignore data-bwignore data-lpignore="true" data-protonpass-ignore="true" autocomplete="off" />
42 <small>
43 If you need a bluesky app password for your account, <a target="_blank" href="https://bsky.app/settings/app-passwords">you can get one here</a>.
44 </small>
···1import { Context } from "hono";
2import { BaseLayout } from "../layout/main";
3import { isUsingInviteKeys } from "../utils/inviteKeys";
4+import { MAX_DASHBOARD_PASS, MIN_DASHBOARD_PASS } from "../limits.d";
5import NavTags from "../layout/navTags";
6import isEmpty from "just-is-empty";
7import AccountHandler from "../layout/account";
8+import { UsernameField } from "../layout/usernameField";
9import TurnstileCaptcha from "../layout/turnstile";
10import FooterCopyright from "../layout/footer";
11+import { BSkyAppPasswordField, DashboardPasswordField } from "../layout/passwordFields";
12+import { PWAutoCompleteSettings } from "../types.d";
1314export default function Signup(props:any) {
15 const ctx: Context = props.c;
···3233 <label>
34 Dashboard Password
35+ <DashboardPasswordField autocomplete={PWAutoCompleteSettings.NewPass} required={true} />
036 <small>Create a new password to use to login to this website. Passwords should be {MIN_DASHBOARD_PASS} to {MAX_DASHBOARD_PASS} characters long.</small>
37 </label>
3839 <label>
40 Bluesky App Password
41+ <BSkyAppPasswordField required={true} />
042 <small>
43 If you need a bluesky app password for your account, <a target="_blank" href="https://bsky.app/settings/app-passwords">you can get one here</a>.
44 </small>
+27-13
src/utils/bskyApi.ts
···1import { type AppBskyFeedPost, AtpAgent, RichText } from '@atproto/api';
2-import { Bindings, Post, Repost, PostLabel, EmbedData, PostResponseObject, LooseObj } from '../types.d';
3import { MAX_ALT_TEXT, MAX_EMBEDS, MAX_LENGTH, MAX_POSTED_LENGTH } from '../limits.d';
4-import { updatePostData, getBskyUserPassForId } from './dbQuery';
5import { deleteEmbedsFromR2 } from './r2Query';
6import {imageDimensionsFromStream} from 'image-dimensions';
7import truncate from "just-truncate";
···22 password: pass,
23 });
24 if (!loginResponse.success) {
25- console.warn(`could not login as user ${user}`);
26- return false;
000000000027 }
28- return true;
29 } catch (err) {
30 console.error(`encountered exception on login for user ${user}, err ${err}`);
31 }
32- return false;
33}
3435export const makeRepost = async (env: Bindings, content: Repost) => {
···45 return false;
46 }
4748- const loginResponse = await loginToBsky(agent, user, pass);
49- if (!loginResponse) {
50- // TODO: Probably should handle failure better here.
0051 return false;
52 }
53···82 return null;
83 }
8485- const loginResponse = await loginToBsky(agent, user, pass);
86- if (!loginResponse) {
87- // TODO: Probably should handle failure better here.
0088 return null;
89 }
90···103 const posts:PostResponseObject[] = [];
104105 const postSegment = async (data: string) => {
106- let postRecord:AppBskyFeedPost.Record = {
107 $type: 'app.bsky.feed.post',
108 text: data,
109 facets: rt.facets,
···1import { type AppBskyFeedPost, AtpAgent, RichText } from '@atproto/api';
2+import { Bindings, Post, Repost, PostLabel, EmbedData, PostResponseObject, LooseObj, PlatformLoginResponse } from '../types.d';
3import { MAX_ALT_TEXT, MAX_EMBEDS, MAX_LENGTH, MAX_POSTED_LENGTH } from '../limits.d';
4+import { updatePostData, getBskyUserPassForId, createViolationForUser } from './dbQuery';
5import { deleteEmbedsFromR2 } from './r2Query';
6import {imageDimensionsFromStream} from 'image-dimensions';
7import truncate from "just-truncate";
···22 password: pass,
23 });
24 if (!loginResponse.success) {
25+ if (loginResponse.data.active == false) {
26+ switch (loginResponse.data.status) {
27+ case "deactivated":
28+ return PlatformLoginResponse.Deactivated;
29+ case "suspended":
30+ return PlatformLoginResponse.Suspended;
31+ case "takendown":
32+ return PlatformLoginResponse.TakenDown;
33+ }
34+ return PlatformLoginResponse.InvalidAccount;
35+ }
36+ return PlatformLoginResponse.PlatformOutage;
37 }
38+ return PlatformLoginResponse.Ok;
39 } catch (err) {
40 console.error(`encountered exception on login for user ${user}, err ${err}`);
41 }
42+ return PlatformLoginResponse.UnhandledError;
43}
4445export const makeRepost = async (env: Bindings, content: Repost) => {
···55 return false;
56 }
5758+ const loginResponse:PlatformLoginResponse = await loginToBsky(agent, user, pass);
59+ if (loginResponse != PlatformLoginResponse.Ok) {
60+ const addViolation:boolean = await createViolationForUser(env, content.userId, loginResponse);
61+ if (addViolation)
62+ console.error(`Unable to login to make repost from user ${content.userId} with violation ${loginResponse}`);
63 return false;
64 }
65···94 return null;
95 }
9697+ const loginResponse:PlatformLoginResponse = await loginToBsky(agent, user, pass);
98+ if (loginResponse != PlatformLoginResponse.Ok) {
99+ const addViolation:boolean = await createViolationForUser(env, content.user, loginResponse);
100+ if (addViolation)
101+ console.error(`Unable to login to make post ${content.postid} with violation ${loginResponse}`);
102 return null;
103 }
104···117 const posts:PostResponseObject[] = [];
118119 const postSegment = async (data: string) => {
120+ let postRecord:AppBskyFeedPost.Record = {
121 $type: 'app.bsky.feed.post',
122 text: data,
123 facets: rt.facets,
+2-2
src/utils/bskyMsg.ts
···1import { AtpAgent, RichText } from '@atproto/api';
2import { loginToBsky } from './bskyApi';
3-import { Bindings } from '../types';
45export const createDMWithUser = async (env: Bindings, user: string, msg: string) => {
6 const agent = new AtpAgent({
···8 });
910 const loginResponse = await loginToBsky(agent, env.RESET_BOT_USERNAME, env.RESET_BOT_APP_PASS);
11- if (!loginResponse) {
12 console.error("Unable to login to the bot to send reset password messages");
13 return false;
14 }
···1import { AtpAgent, RichText } from '@atproto/api';
2import { loginToBsky } from './bskyApi';
3+import { Bindings, PlatformLoginResponse } from '../types.d';
45export const createDMWithUser = async (env: Bindings, user: string, msg: string) => {
6 const agent = new AtpAgent({
···8 });
910 const loginResponse = await loginToBsky(agent, env.RESET_BOT_USERNAME, env.RESET_BOT_APP_PASS);
11+ if (loginResponse != PlatformLoginResponse.Ok) {
12 console.error("Unable to login to the bot to send reset password messages");
13 return false;
14 }
+74-10
src/utils/dbQuery.ts
···1import { Context } from "hono";
2import { DrizzleD1Database, drizzle } from "drizzle-orm/d1";
3-import { sql, and, or, gt, eq, lte, inArray, desc, count, getTableColumns } from "drizzle-orm";
4import { BatchItem } from "drizzle-orm/batch";
5-import { posts, reposts } from "../db/app.schema";
6import { accounts, users } from "../db/auth.schema";
7import { PostSchema } from "../validation/postSchema";
8-import { Bindings } from "../types";
9import { MAX_POSTED_LENGTH } from "../limits.d";
10import { createPostObject, floorCurrentTime, floorGivenTime } from "./helpers";
11import { deleteEmbedsFromR2 } from "./r2Query";
···67 if (userData) {
68 const db: DrizzleD1Database = drizzle(c.env.DB);
69 let queriesToExecute:BatchItem<"sqlite">[] = [];
07071- if (has(newData, "password")) {
72 // cache out the new hash
73 const newPassword = newData.password;
74 // remove it from the original object
···80 .where(eq(accounts.userId, userData.id)));
81 }
820000083 if (!isEmpty(newData)) {
84 queriesToExecute.push(db.update(users).set(newData)
85 .where(eq(users.id, userData.id)));
···136 return { ok: false, msg: "Scheduled date must be in the future" };
137 }
13800139 const postUUID = uuidv4();
140 let dbOperations:BatchItem<"sqlite">[] = [
141 db.insert(posts).values({
···168 const db: DrizzleD1Database = drizzle(env.DB);
169 const currentTime: Date = floorCurrentTime();
1700171 return await db.select().from(posts)
172- .where(and(lte(posts.scheduledDate, currentTime),
173- eq(posts.posted, false)))
174- .all();
0175};
176177export const getAllRepostsForGivenTime = async (env: Bindings, givenDate: Date) => {
···179 const db: DrizzleD1Database = drizzle(env.DB);
180 const query = db.select({uuid: reposts.uuid}).from(reposts)
181 .where(lte(reposts.scheduledDate, givenDate));
0182 return await db.select({uri: posts.uri, cid: posts.cid, userId: posts.userId })
183 .from(posts)
184- .where(inArray(posts.uuid, query))
185 .all();
186};
187···206 return false;
207208 const db: DrizzleD1Database = drizzle(c.env.DB);
209- const result = await db.update(posts).set(newData).where(and(eq(posts.uuid, id), eq(posts.userId, userData.id)));
210- return result.success;
211};
212213export const getPostById = async(c: Context, id: string) => {
···287 postTruncation.forEach(async item => {
288 await db.update(posts).set({ content: truncate(item.content, MAX_POSTED_LENGTH) }).where(eq(posts.uuid, item.id));
289 });
00000000000000000000000000000000000000000000000000000290 }
291};
···1import { Context } from "hono";
2import { DrizzleD1Database, drizzle } from "drizzle-orm/d1";
3+import { sql, and, gt, eq, lte, inArray, desc, count, getTableColumns, notInArray, ne } from "drizzle-orm";
4import { BatchItem } from "drizzle-orm/batch";
5+import { posts, reposts, violations } from "../db/app.schema";
6import { accounts, users } from "../db/auth.schema";
7import { PostSchema } from "../validation/postSchema";
8+import { Bindings, LooseObj, PlatformLoginResponse } from "../types.d";
9import { MAX_POSTED_LENGTH } from "../limits.d";
10import { createPostObject, floorCurrentTime, floorGivenTime } from "./helpers";
11import { deleteEmbedsFromR2 } from "./r2Query";
···67 if (userData) {
68 const db: DrizzleD1Database = drizzle(c.env.DB);
69 let queriesToExecute:BatchItem<"sqlite">[] = [];
70+ const updatedPassword = has(newData, "password");
7172+ if (updatedPassword) {
73 // cache out the new hash
74 const newPassword = newData.password;
75 // remove it from the original object
···81 .where(eq(accounts.userId, userData.id)));
82 }
8384+ // If we have new data about the username, pds, or password, then clear account invalid violations
85+ if (updatedPassword || has(newData, "username") || has(newData, "pds")) {
86+ queriesToExecute.push(getViolationDeleteQueryForUser(db, userData.id));
87+ }
88+89 if (!isEmpty(newData)) {
90 queriesToExecute.push(db.update(users).set(newData)
91 .where(eq(users.id, userData.id)));
···142 return { ok: false, msg: "Scheduled date must be in the future" };
143 }
144145+ // TODO: prevent anything from happening if you are currently in violations table
146+147 const postUUID = uuidv4();
148 let dbOperations:BatchItem<"sqlite">[] = [
149 db.insert(posts).values({
···176 const db: DrizzleD1Database = drizzle(env.DB);
177 const currentTime: Date = floorCurrentTime();
178179+ const violationUsers = db.select({data: violations.userId}).from(violations);
180 return await db.select().from(posts)
181+ .where(and(and(
182+ lte(posts.scheduledDate, currentTime), eq(posts.posted, false)),
183+ notInArray(posts.userId, violationUsers))
184+ ).all();
185};
186187export const getAllRepostsForGivenTime = async (env: Bindings, givenDate: Date) => {
···189 const db: DrizzleD1Database = drizzle(env.DB);
190 const query = db.select({uuid: reposts.uuid}).from(reposts)
191 .where(lte(reposts.scheduledDate, givenDate));
192+ const violationsQuery = db.select({data: violations.userId}).from(violations);
193 return await db.select({uri: posts.uri, cid: posts.cid, userId: posts.userId })
194 .from(posts)
195+ .where(and(inArray(posts.uuid, query), notInArray(posts.userId, violationsQuery)))
196 .all();
197};
198···217 return false;
218219 const db: DrizzleD1Database = drizzle(c.env.DB);
220+ const {success} = await db.update(posts).set(newData).where(and(eq(posts.uuid, id), eq(posts.userId, userData.id)));
221+ return success;
222};
223224export const getPostById = async(c: Context, id: string) => {
···298 postTruncation.forEach(async item => {
299 await db.update(posts).set({ content: truncate(item.content, MAX_POSTED_LENGTH) }).where(eq(posts.uuid, item.id));
300 });
301+ }
302+};
303+304+export const createViolationForUser = async(env: Bindings, userId: string, violationType: PlatformLoginResponse) => {
305+ const NoHandleState:PlatformLoginResponse[] = [PlatformLoginResponse.Ok, PlatformLoginResponse.PlatformOutage, PlatformLoginResponse.None, PlatformLoginResponse.UnhandledError];
306+ // Don't do anything in these cases
307+ if (violationType in NoHandleState) {
308+ console.warn(`createViolationForUser got a not valid add request for user ${userId} with violation ${violationType}`);
309+ return false;
310+ }
311+312+ const db: DrizzleD1Database = drizzle(env.DB);
313+ let valuesUpdate:LooseObj = {};
314+ switch (violationType)
315+ {
316+ case PlatformLoginResponse.InvalidAccount:
317+ case PlatformLoginResponse.InvalidCreds:
318+ valuesUpdate.userPassInvalid = true;
319+ break;
320+ case PlatformLoginResponse.Suspended:
321+ valuesUpdate.accountSuspended = true;
322+ break;
323+ case PlatformLoginResponse.TakenDown:
324+ case PlatformLoginResponse.Deactivated:
325+ valuesUpdate.accountGone = true;
326+ break;
327+ default:
328+ console.warn(`createViolationForUser was not properly handled for ${violationType}`);
329+ return false;
330+ }
331+332+ const {success} = await db.insert(violations).values({userId: userId, ...valuesUpdate}).onConflictDoUpdate({target: violations.userId, set: valuesUpdate});
333+ return success;
334+};
335+336+const getViolationDeleteQueryForUser = (db:DrizzleD1Database, userId: string) => {
337+ return db.delete(violations).where(and(eq(violations.userId, userId),
338+ and(ne(violations.tosViolation, true), ne(violations.accountGone, true))));
339+};
340+341+export const clearViolationForUser = async(env: Bindings, userId: string) => {
342+ const db: DrizzleD1Database = drizzle(env.DB);
343+ const {success} = await getViolationDeleteQueryForUser(db, userId);
344+ return success;
345+};
346+347+export const getViolationsForCurrentUser = async(c: Context) => {
348+ const userData = c.get("user");
349+ if (userData) {
350+ const db: DrizzleD1Database = drizzle(c.env.DB);
351+ return await db.select().from(violations).where(eq(violations.userId, userData.id)).limit(1).run();
352+ } else {
353+ return {success: false, results: []};
354 }
355};