Merge branch 'main' into feat/add-short.io
This commit is contained in:
commit
052fed90ca
|
|
@ -1,9 +1,11 @@
|
|||
# Configuration reference: http://docs.postiz.com/configuration/reference
|
||||
|
||||
# === Required Settings
|
||||
# === Required Settings
|
||||
DATABASE_URL="postgresql://postiz-user:postiz-password@localhost:5432/postiz-db-local"
|
||||
REDIS_URL="redis://localhost:6379"
|
||||
JWT_SECRET="random string for your JWT secret, make it long"
|
||||
|
||||
# === This needs to be exactly the URL you're accessing Postiz on.
|
||||
FRONTEND_URL="http://localhost:4200"
|
||||
NEXT_PUBLIC_BACKEND_URL="http://localhost:3000"
|
||||
BACKEND_INTERNAL_URL="http://localhost:3000"
|
||||
|
|
@ -77,6 +79,7 @@ MASTODON_CLIENT_SECRET=""
|
|||
OPENAI_API_KEY=""
|
||||
NEXT_PUBLIC_DISCORD_SUPPORT=""
|
||||
NEXT_PUBLIC_POLOTNO=""
|
||||
NOT_SECURED=false
|
||||
|
||||
# Payment settings
|
||||
FEE_AMOUNT=0.05
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ The Postiz app is committed to ensuring the security and integrity of our users'
|
|||
If you discover a security vulnerability in the Postiz app, please report it to us privately via email to one of the maintainers:
|
||||
|
||||
* @nevo-david
|
||||
* @jamesread ([email](mailto:contact@jread.com))
|
||||
* @jonathan-irvin ([email](mailto:offendingcommit@gmail.com))
|
||||
* @egelhaus ([email](mailto:gelhausenno@outlook.de))
|
||||
|
||||
When reporting a security vulnerability, please provide as much detail as possible, including:
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ export class AuthController {
|
|||
private _authService: AuthService,
|
||||
private _emailService: EmailService
|
||||
) {}
|
||||
|
||||
@Get('/can-register')
|
||||
async canRegister() {
|
||||
return { register: await this._authService.canRegister() };
|
||||
}
|
||||
|
||||
@Post('/register')
|
||||
async register(
|
||||
@Req() req: Request,
|
||||
|
|
@ -60,20 +66,36 @@ export class AuthController {
|
|||
|
||||
response.cookie('auth', jwt, {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
}
|
||||
: {}),
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
|
||||
});
|
||||
|
||||
if (process.env.NOT_SECURED) {
|
||||
response.header('auth', jwt);
|
||||
}
|
||||
|
||||
if (typeof addedOrg !== 'boolean' && addedOrg?.organizationId) {
|
||||
response.cookie('showorg', addedOrg.organizationId, {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
}
|
||||
: {}),
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
|
||||
});
|
||||
|
||||
if (process.env.NOT_SECURED) {
|
||||
response.header('showorg', addedOrg.organizationId);
|
||||
}
|
||||
}
|
||||
|
||||
response.header('onboarding', 'true');
|
||||
|
|
@ -108,20 +130,36 @@ export class AuthController {
|
|||
|
||||
response.cookie('auth', jwt, {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
}
|
||||
: {}),
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
|
||||
});
|
||||
|
||||
if (process.env.NOT_SECURED) {
|
||||
response.header('auth', jwt);
|
||||
}
|
||||
|
||||
if (typeof addedOrg !== 'boolean' && addedOrg?.organizationId) {
|
||||
response.cookie('showorg', addedOrg.organizationId, {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
}
|
||||
: {}),
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
|
||||
});
|
||||
|
||||
if (process.env.NOT_SECURED) {
|
||||
response.header('showorg', addedOrg.organizationId);
|
||||
}
|
||||
}
|
||||
|
||||
response.header('reload', 'true');
|
||||
|
|
@ -172,12 +210,20 @@ export class AuthController {
|
|||
|
||||
response.cookie('auth', activate, {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
}
|
||||
: {}),
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
|
||||
});
|
||||
|
||||
if (process.env.NOT_SECURED) {
|
||||
response.header('auth', activate);
|
||||
}
|
||||
|
||||
response.header('onboarding', 'true');
|
||||
return response.status(200).send({ can: true });
|
||||
}
|
||||
|
|
@ -195,12 +241,20 @@ export class AuthController {
|
|||
|
||||
response.cookie('auth', jwt, {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
}
|
||||
: {}),
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
|
||||
});
|
||||
|
||||
if (process.env.NOT_SECURED) {
|
||||
response.header('auth', jwt);
|
||||
}
|
||||
|
||||
response.header('reload', 'true');
|
||||
|
||||
response.status(200).json({
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import {
|
|||
RefreshToken,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { TelegramProvider } from '@gitroom/nestjs-libraries/integrations/social/telegram.provider';
|
||||
|
||||
@ApiTags('Integrations')
|
||||
@Controller('/integrations')
|
||||
|
|
@ -85,30 +86,37 @@ export class IntegrationsController {
|
|||
@Get('/list')
|
||||
async getIntegrationList(@GetOrgFromRequest() org: Organization) {
|
||||
return {
|
||||
integrations: (
|
||||
await this._integrationService.getIntegrationsList(org.id)
|
||||
).map((p) => {
|
||||
const findIntegration = this._integrationManager.getSocialIntegration(
|
||||
p.providerIdentifier
|
||||
);
|
||||
return {
|
||||
name: p.name,
|
||||
id: p.id,
|
||||
internalId: p.internalId,
|
||||
disabled: p.disabled,
|
||||
picture: p.picture || '/no-picture.jpg',
|
||||
identifier: p.providerIdentifier,
|
||||
inBetweenSteps: p.inBetweenSteps,
|
||||
refreshNeeded: p.refreshNeeded,
|
||||
display: p.profile,
|
||||
type: p.type,
|
||||
time: JSON.parse(p.postingTimes),
|
||||
changeProfilePicture: !!findIntegration?.changeProfilePicture,
|
||||
changeNickName: !!findIntegration?.changeNickname,
|
||||
customer: p.customer,
|
||||
additionalSettings: p.additionalSettings || '[]',
|
||||
};
|
||||
}),
|
||||
integrations: await Promise.all(
|
||||
(await this._integrationService.getIntegrationsList(org.id)).map(
|
||||
async (p) => {
|
||||
const findIntegration =
|
||||
this._integrationManager.getSocialIntegration(
|
||||
p.providerIdentifier
|
||||
);
|
||||
return {
|
||||
name: p.name,
|
||||
id: p.id,
|
||||
internalId: p.internalId,
|
||||
disabled: p.disabled,
|
||||
picture: p.picture || '/no-picture.jpg',
|
||||
identifier: p.providerIdentifier,
|
||||
inBetweenSteps: p.inBetweenSteps,
|
||||
refreshNeeded: p.refreshNeeded,
|
||||
isCustomFields: !!findIntegration.customFields,
|
||||
...(findIntegration.customFields
|
||||
? { customFields: await findIntegration.customFields() }
|
||||
: {}),
|
||||
display: p.profile,
|
||||
type: p.type,
|
||||
time: JSON.parse(p.postingTimes),
|
||||
changeProfilePicture: !!findIntegration?.changeProfilePicture,
|
||||
changeNickName: !!findIntegration?.changeNickname,
|
||||
customer: p.customer,
|
||||
additionalSettings: p.additionalSettings || '[]',
|
||||
};
|
||||
}
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -609,4 +617,9 @@ export class IntegrationsController {
|
|||
) {
|
||||
return this._integrationService.changePlugActivation(org.id, id, status);
|
||||
}
|
||||
|
||||
@Get('/telegram/updates')
|
||||
async getUpdates(@Query() query: { word: string; id?: number }) {
|
||||
return new TelegramProvider().getBotId(query);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,8 +101,12 @@ export class PublicController {
|
|||
if (!req.cookies.track) {
|
||||
res.cookie('track', uniqueId, {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
}
|
||||
: {}),
|
||||
sameSite: 'none',
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
|
||||
});
|
||||
|
|
@ -111,8 +115,12 @@ export class PublicController {
|
|||
if (body.fbclid && !req.cookies.fbclid) {
|
||||
res.cookie('fbclid', body.fbclid, {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
}
|
||||
: {}),
|
||||
sameSite: 'none',
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -48,11 +48,13 @@ export class UsersController {
|
|||
async getSelf(
|
||||
@GetUserFromRequest() user: User,
|
||||
@GetOrgFromRequest() organization: Organization,
|
||||
@Req() req: Request,
|
||||
@Req() req: Request
|
||||
) {
|
||||
if (!organization) {
|
||||
throw new HttpForbiddenException();
|
||||
}
|
||||
|
||||
const impersonate = req.cookies.impersonate || req.headers.impersonate;
|
||||
// @ts-ignore
|
||||
return {
|
||||
...user,
|
||||
|
|
@ -67,12 +69,10 @@ export class UsersController {
|
|||
// @ts-ignore
|
||||
isLifetime: !!organization?.subscription?.isLifetime,
|
||||
admin: !!user.isSuperAdmin,
|
||||
impersonate: !!req.cookies.impersonate,
|
||||
impersonate: !!impersonate,
|
||||
allowTrial: organization?.allowTrial,
|
||||
// @ts-ignore
|
||||
publicApi: organization?.users[0]?.role === 'SUPERADMIN' || organization?.users[0]?.role === 'ADMIN'
|
||||
? organization?.apiKey
|
||||
: '',
|
||||
publicApi: organization?.users[0]?.role === 'SUPERADMIN' || organization?.users[0]?.role === 'ADMIN' ? organization?.apiKey : '',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -105,11 +105,19 @@ export class UsersController {
|
|||
|
||||
response.cookie('impersonate', id, {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
}
|
||||
: {}),
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
|
||||
});
|
||||
|
||||
if (process.env.NOT_SECURED) {
|
||||
response.header('impersonate', id);
|
||||
}
|
||||
}
|
||||
|
||||
@Post('/personal')
|
||||
|
|
@ -175,12 +183,20 @@ export class UsersController {
|
|||
) {
|
||||
response.cookie('showorg', id, {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
}
|
||||
: {}),
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
|
||||
});
|
||||
|
||||
if (process.env.NOT_SECURED) {
|
||||
response.header('showorg', id);
|
||||
}
|
||||
|
||||
response.status(200).send();
|
||||
}
|
||||
|
||||
|
|
@ -188,29 +204,41 @@ export class UsersController {
|
|||
logout(@Res({ passthrough: true }) response: Response) {
|
||||
response.cookie('auth', '', {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
}
|
||||
: {}),
|
||||
maxAge: -1,
|
||||
expires: new Date(0),
|
||||
sameSite: 'none',
|
||||
});
|
||||
|
||||
response.cookie('showorg', '', {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
}
|
||||
: {}),
|
||||
maxAge: -1,
|
||||
expires: new Date(0),
|
||||
sameSite: 'none',
|
||||
});
|
||||
|
||||
response.cookie('impersonate', '', {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
}
|
||||
: {}),
|
||||
maxAge: -1,
|
||||
expires: new Date(0),
|
||||
sameSite: 'none',
|
||||
});
|
||||
|
||||
response.status(200).send();
|
||||
|
|
@ -223,22 +251,34 @@ export class UsersController {
|
|||
@GetUserFromRequest() user: User,
|
||||
@RealIP() ip: string,
|
||||
@UserAgent() userAgent: string,
|
||||
@Body() body: { tt: TrackEnum; fbclid: string, additional: Record<string, any> }
|
||||
@Body()
|
||||
body: { tt: TrackEnum; fbclid: string; additional: Record<string, any> }
|
||||
) {
|
||||
const uniqueId = req?.cookies?.track || makeId(10);
|
||||
const fbclid = req?.cookies?.fbclid || body.fbclid;
|
||||
await this._trackService.track(uniqueId, ip, userAgent, body.tt, body.additional, fbclid, user);
|
||||
await this._trackService.track(
|
||||
uniqueId,
|
||||
ip,
|
||||
userAgent,
|
||||
body.tt,
|
||||
body.additional,
|
||||
fbclid,
|
||||
user
|
||||
);
|
||||
if (!req.cookies.track) {
|
||||
res.cookie('track', uniqueId, {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
}
|
||||
: {}),
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
|
||||
});
|
||||
}
|
||||
|
||||
console.log('hello');
|
||||
res.status(200).json({
|
||||
track: uniqueId,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,8 +14,13 @@ async function bootstrap() {
|
|||
const app = await NestFactory.create(AppModule, {
|
||||
rawBody: true,
|
||||
cors: {
|
||||
credentials: true,
|
||||
exposedHeaders: ['reload', 'onboarding', 'activate'],
|
||||
...(!process.env.NOT_SECURED ? { credentials: true } : {}),
|
||||
exposedHeaders: [
|
||||
'reload',
|
||||
'onboarding',
|
||||
'activate',
|
||||
...(process.env.NOT_SECURED ? ['auth', 'showorg', 'impersonate'] : []),
|
||||
],
|
||||
origin: [
|
||||
process.env.FRONTEND_URL,
|
||||
...(process.env.MAIN_URL ? [process.env.MAIN_URL] : []),
|
||||
|
|
@ -39,8 +44,8 @@ async function bootstrap() {
|
|||
|
||||
try {
|
||||
await app.listen(port);
|
||||
|
||||
checkConfiguration() // Do this last, so that users will see obvious issues at the end of the startup log without having to scroll up.
|
||||
|
||||
checkConfiguration(); // Do this last, so that users will see obvious issues at the end of the startup log without having to scroll up.
|
||||
|
||||
Logger.log(`🚀 Backend is running on: http://localhost:${port}`);
|
||||
} catch (e) {
|
||||
|
|
@ -50,17 +55,17 @@ async function bootstrap() {
|
|||
|
||||
function checkConfiguration() {
|
||||
const checker = new ConfigurationChecker();
|
||||
checker.readEnvFromProcess()
|
||||
checker.check()
|
||||
checker.readEnvFromProcess();
|
||||
checker.check();
|
||||
|
||||
if (checker.hasIssues()) {
|
||||
for (const issue of checker.getIssues()) {
|
||||
Logger.warn(issue, 'Configuration issue')
|
||||
Logger.warn(issue, 'Configuration issue');
|
||||
}
|
||||
|
||||
Logger.warn("Configuration issues found: " + checker.getIssuesCount())
|
||||
Logger.warn('Configuration issues found: ' + checker.getIssuesCount());
|
||||
} else {
|
||||
Logger.log("Configuration check completed without any issues.")
|
||||
Logger.log('Configuration check completed without any issues.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,13 @@ import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/excep
|
|||
export const removeAuth = (res: Response) => {
|
||||
res.cookie('auth', '', {
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: 'none',
|
||||
}
|
||||
: {}),
|
||||
expires: new Date(0),
|
||||
maxAge: -1,
|
||||
});
|
||||
|
|
@ -43,9 +47,10 @@ export class AuthMiddleware implements NestMiddleware {
|
|||
throw new HttpForbiddenException();
|
||||
}
|
||||
|
||||
if (user?.isSuperAdmin && req.cookies.impersonate) {
|
||||
const impersonate = req.cookies.impersonate || req.headers.impersonate;
|
||||
if (user?.isSuperAdmin && impersonate) {
|
||||
const loadImpersonate = await this._organizationService.getUserOrg(
|
||||
req.cookies.impersonate
|
||||
impersonate
|
||||
);
|
||||
|
||||
if (loadImpersonate) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,14 @@ export class AuthService {
|
|||
private _notificationService: NotificationService,
|
||||
private _emailService: EmailService
|
||||
) {}
|
||||
async canRegister() {
|
||||
if (!process.env.DISABLE_REGISTRATION) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (await this._organizationService.getCount()) === 0;
|
||||
}
|
||||
|
||||
async routeAuth(
|
||||
provider: Provider,
|
||||
body: CreateOrgUserDto | LoginUserDto,
|
||||
|
|
@ -34,6 +42,10 @@ export class AuthService {
|
|||
throw new Error('User already exists');
|
||||
}
|
||||
|
||||
if (!(await this.canRegister())) {
|
||||
throw new Error('Registration is disabled');
|
||||
}
|
||||
|
||||
const create = await this._organizationService.createOrgAndUser(
|
||||
body,
|
||||
ip,
|
||||
|
|
@ -132,6 +144,10 @@ export class AuthService {
|
|||
return user;
|
||||
}
|
||||
|
||||
if (!(await this.canRegister())) {
|
||||
throw new Error('Registration is disabled');
|
||||
}
|
||||
|
||||
const create = await this._organizationService.createOrgAndUser(
|
||||
{
|
||||
company: body.company,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
|
||||
import { NeynarAPIClient } from '@neynar/nodejs-sdk';
|
||||
|
||||
const client = new NeynarAPIClient({
|
||||
apiKey: process.env.NEYNAR_SECRET_KEY || '00000000-000-0000-000-000000000000',
|
||||
});
|
||||
|
||||
export class FarcasterProvider implements ProvidersInterface {
|
||||
generateLink() {
|
||||
return '';
|
||||
}
|
||||
|
||||
async getToken(code: string) {
|
||||
const data = JSON.parse(Buffer.from(code, 'base64').toString());
|
||||
const status = await client.lookupSigner({signerUuid: data.signer_uuid});
|
||||
if (status.status === 'approved') {
|
||||
return data.signer_uuid;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
async getUser(providerToken: string) {
|
||||
const status = await client.lookupSigner({signerUuid: providerToken});
|
||||
if (status.status !== 'approved') {
|
||||
return {
|
||||
id: '',
|
||||
email: '',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// const { client, oauth2 } = clientAndYoutube();
|
||||
// client.setCredentials({ access_token: providerToken });
|
||||
// const user = oauth2(client);
|
||||
// const { data } = await user.userinfo.get();
|
||||
|
||||
return {
|
||||
id: String('farcaster_' + status.fid),
|
||||
email: String('farcaster_' + status.fid),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { Provider } from '@prisma/client';
|
|||
import { GithubProvider } from '@gitroom/backend/services/auth/providers/github.provider';
|
||||
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
|
||||
import { GoogleProvider } from '@gitroom/backend/services/auth/providers/google.provider';
|
||||
import { FarcasterProvider } from '@gitroom/backend/services/auth/providers/farcaster.provider';
|
||||
|
||||
export class ProvidersFactory {
|
||||
static loadProvider(provider: Provider): ProvidersInterface {
|
||||
|
|
@ -10,6 +11,8 @@ export class ProvidersFactory {
|
|||
return new GithubProvider();
|
||||
case Provider.GOOGLE:
|
||||
return new GoogleProvider();
|
||||
case Provider.FARCASTER:
|
||||
return new FarcasterProvider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -68,7 +68,7 @@ export default async function AuthLayout({
|
|||
<div className="absolute top-0 bg-gradient-to-t from-customColor9 w-[1px] translate-x-[22px] h-full" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="absolute right-0 bg-gradient-to-l from-customColor9 h-[1px] translate-y-[22px] w-full" />
|
||||
<div className="absolute right-0 bg-gradient-to-l from-customColor9 h-[1px] translate-y-[60px] w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-0 bg-gradient-to-t from-customColor9 w-[1px] -translate-x-[22px] h-full" />
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { internalFetch } from '@gitroom/helpers/utils/internal.fetch';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { Register } from '@gitroom/frontend/components/auth/register';
|
||||
import { Metadata } from 'next';
|
||||
import { isGeneralServerSide } from '@gitroom/helpers/utils/is.general.server.side';
|
||||
import Link from 'next/link';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `${isGeneralServerSide() ? 'Postiz' : 'Gitroom'} Register`,
|
||||
|
|
@ -11,5 +13,20 @@ export const metadata: Metadata = {
|
|||
};
|
||||
|
||||
export default async function Auth() {
|
||||
if (process.env.DISABLE_REGISTRATION) {
|
||||
const canRegister = (
|
||||
await (await internalFetch('/auth/can-register')).json()
|
||||
).register;
|
||||
if (!canRegister) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
Registration is disabled
|
||||
<br />
|
||||
<Link className="underline hover:font-bold" href="/auth/login">Login instead</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <Register />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
uploadDirectory={process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY!}
|
||||
tolt={process.env.NEXT_PUBLIC_TOLT!}
|
||||
facebookPixel={process.env.NEXT_PUBLIC_FACEBOOK_PIXEL!}
|
||||
telegramBotName={process.env.TELEGRAM_BOT_NAME!}
|
||||
neynarClientId={process.env.NEYNAR_CLIENT_ID!}
|
||||
isSecured={!process.env.NOT_SECURED}
|
||||
>
|
||||
<ToltScript />
|
||||
<FacebookComponent />
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { GithubProvider } from '@gitroom/frontend/components/auth/providers/gith
|
|||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
import { GoogleProvider } from '@gitroom/frontend/components/auth/providers/google.provider';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { FarcasterProvider } from '@gitroom/frontend/components/auth/providers/farcaster.provider';
|
||||
|
||||
type Inputs = {
|
||||
email: string;
|
||||
|
|
@ -22,7 +23,7 @@ type Inputs = {
|
|||
|
||||
export function Login() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const {isGeneral} = useVariables();
|
||||
const { isGeneral, neynarClientId } = useVariables();
|
||||
const resolver = useMemo(() => {
|
||||
return classValidatorResolver(LoginUserDto);
|
||||
}, []);
|
||||
|
|
@ -62,7 +63,14 @@ export function Login() {
|
|||
</h1>
|
||||
</div>
|
||||
|
||||
{!isGeneral ? <GithubProvider /> : <GoogleProvider />}
|
||||
{!isGeneral ? (
|
||||
<GithubProvider />
|
||||
) : (
|
||||
<div className="gap-[5px] flex flex-col">
|
||||
<GoogleProvider />
|
||||
{!!neynarClientId && <FarcasterProvider />}
|
||||
</div>
|
||||
)}
|
||||
<div className="h-[20px] mb-[24px] mt-[24px] relative">
|
||||
<div className="absolute w-full h-[1px] bg-fifth top-[50%] -translate-y-[50%]" />
|
||||
<div
|
||||
|
|
@ -89,7 +97,11 @@ export function Login() {
|
|||
</div>
|
||||
<div className="text-center mt-6">
|
||||
<div className="w-full flex">
|
||||
<Button type="submit" className="flex-1 rounded-[4px]" loading={loading}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="flex-1 rounded-[4px]"
|
||||
loading={loading}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
'use client';
|
||||
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
useRef,
|
||||
FC,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import { useNeynarContext } from '@neynar/react';
|
||||
|
||||
export const NeynarAuthButton: FC<{
|
||||
children: ReactNode;
|
||||
onLogin: (code: string) => void;
|
||||
}> = (props) => {
|
||||
const { children, onLogin } = props;
|
||||
const { client_id } = useNeynarContext();
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const authWindowRef = useRef<Window | null>(null);
|
||||
const neynarLoginUrl = `${
|
||||
process.env.NEYNAR_LOGIN_URL ?? 'https://app.neynar.com/login'
|
||||
}?client_id=${client_id}`;
|
||||
const authOrigin = new URL(neynarLoginUrl).origin;
|
||||
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMessage = useCallback(
|
||||
async (event: MessageEvent) => {
|
||||
if (
|
||||
event.origin === authOrigin &&
|
||||
event.data &&
|
||||
event.data.is_authenticated
|
||||
) {
|
||||
authWindowRef.current?.close();
|
||||
window.removeEventListener('message', handleMessage); // Remove listener here
|
||||
const _user = {
|
||||
signer_uuid: event.data.signer_uuid,
|
||||
...event.data.user,
|
||||
};
|
||||
|
||||
onLogin(Buffer.from(JSON.stringify(_user)).toString('base64'));
|
||||
}
|
||||
},
|
||||
[client_id, onLogin]
|
||||
);
|
||||
|
||||
const handleSignIn = useCallback(() => {
|
||||
const width = 600,
|
||||
height = 700;
|
||||
const left = window.screen.width / 2 - width / 2;
|
||||
const top = window.screen.height / 2 - height / 2;
|
||||
const windowFeatures = `width=${width},height=${height},top=${top},left=${left}`;
|
||||
|
||||
authWindowRef.current = window.open(
|
||||
neynarLoginUrl,
|
||||
'_blank',
|
||||
windowFeatures
|
||||
);
|
||||
|
||||
if (!authWindowRef.current) {
|
||||
console.error(
|
||||
'Failed to open the authentication window. Please check your pop-up blocker settings.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('message', handleMessage, false);
|
||||
}, [client_id, handleMessage]);
|
||||
|
||||
const closeModal = () => setShowModal(false);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
window.removeEventListener('message', handleMessage); // Cleanup function to remove listener
|
||||
};
|
||||
}, [handleMessage]);
|
||||
|
||||
const handleOutsideClick = useCallback((event: any) => {
|
||||
if (modalRef.current && !modalRef.current.contains(event.target)) {
|
||||
closeModal();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (showModal) {
|
||||
document.addEventListener('mousedown', handleOutsideClick);
|
||||
} else {
|
||||
document.removeEventListener('mousedown', handleOutsideClick);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleOutsideClick);
|
||||
};
|
||||
}, [showModal, handleOutsideClick]);
|
||||
|
||||
return <div onClick={handleSignIn}>{children}</div>;
|
||||
};
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { FC, useCallback } from 'react';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { NeynarContextProvider, Theme, useNeynarContext } from '@neynar/react';
|
||||
import { NeynarAuthButton } from '@gitroom/frontend/components/auth/nayner.auth.button';
|
||||
|
||||
export const FarcasterProvider = () => {
|
||||
const gotoLogin = useCallback(async (code: string) => {
|
||||
window.location.href = `/auth?provider=FARCASTER&code=${code}`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ButtonCaster login={gotoLogin} />
|
||||
);
|
||||
};
|
||||
|
||||
export const ButtonCaster: FC<{ login: (code: string) => void }> = (props) => {
|
||||
const { login } = props;
|
||||
const { neynarClientId } = useVariables();
|
||||
return (
|
||||
<NeynarContextProvider
|
||||
settings={{
|
||||
clientId: neynarClientId,
|
||||
defaultTheme: Theme.Dark,
|
||||
}}
|
||||
>
|
||||
<NeynarAuthButton onLogin={login}>
|
||||
<div
|
||||
className={`cursor-pointer bg-[#855ECD] h-[44px] rounded-[4px] flex justify-center items-center text-white ${interClass} gap-[4px]`}
|
||||
>
|
||||
<svg
|
||||
width="21px"
|
||||
height="21px"
|
||||
viewBox="0 0 1000 1000"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M257.778 155.556H742.222V844.445H671.111V528.889H670.414C662.554 441.677 589.258 373.333 500 373.333C410.742 373.333 337.446 441.677 329.586 528.889H328.889V844.445H257.778V155.556Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M128.889 253.333L157.778 351.111H182.222V746.667C169.949 746.667 160 756.616 160 768.889V795.556H155.556C143.283 795.556 133.333 805.505 133.333 817.778V844.445H382.222V817.778C382.222 805.505 372.273 795.556 360 795.556H355.556V768.889C355.556 756.616 345.606 746.667 333.333 746.667H306.667V253.333H128.889Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M675.556 746.667C663.282 746.667 653.333 756.616 653.333 768.889V795.556H648.889C636.616 795.556 626.667 805.505 626.667 817.778V844.445H875.556V817.778C875.556 805.505 865.606 795.556 853.333 795.556H848.889V768.889C848.889 756.616 838.94 746.667 826.667 746.667V351.111H851.111L880 253.333H702.222V746.667H675.556Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
<div>Continue with Farcaster</div>
|
||||
</div>
|
||||
</NeynarAuthButton>
|
||||
</NeynarContextProvider>
|
||||
);
|
||||
};
|
||||
|
|
@ -39,7 +39,7 @@ export const GoogleProvider = () => {
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>Sign in with Google</div>
|
||||
<div>Continue with Google</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
|
|||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { useTrack } from '@gitroom/react/helpers/use.track';
|
||||
import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum';
|
||||
import { FarcasterProvider } from '@gitroom/frontend/components/auth/providers/farcaster.provider';
|
||||
|
||||
type Inputs = {
|
||||
email: string;
|
||||
|
|
@ -85,7 +86,7 @@ export function RegisterAfter({
|
|||
token: string;
|
||||
provider: string;
|
||||
}) {
|
||||
const { isGeneral } = useVariables();
|
||||
const { isGeneral, neynarClientId } = useVariables();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
const fireEvents = useFireEvents();
|
||||
|
|
@ -153,7 +154,14 @@ export function RegisterAfter({
|
|||
</h1>
|
||||
</div>
|
||||
{!isAfterProvider &&
|
||||
(!isGeneral ? <GithubProvider /> : <GoogleProvider />)}
|
||||
(!isGeneral ? (
|
||||
<GithubProvider />
|
||||
) : (
|
||||
<div className="gap-[5px] flex flex-col">
|
||||
<GoogleProvider />
|
||||
{!!neynarClientId && <FarcasterProvider />}
|
||||
</div>
|
||||
))}
|
||||
{!isAfterProvider && (
|
||||
<div className="h-[20px] mb-[24px] mt-[24px] relative">
|
||||
<div className="absolute w-full h-[1px] bg-fifth top-[50%] -translate-y-[50%]" />
|
||||
|
|
|
|||
|
|
@ -595,7 +595,7 @@ export const AddEditModal: FC<{
|
|||
)}
|
||||
>
|
||||
<Image
|
||||
src={selectedIntegrations?.[0]?.picture}
|
||||
src={selectedIntegrations?.[0]?.picture || '/no-picture.jpg'}
|
||||
className="rounded-full"
|
||||
alt={selectedIntegrations?.[0]?.identifier}
|
||||
width={32}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export const BotPicture: FC<{
|
|||
const modal = useModals();
|
||||
const toast = useToaster();
|
||||
const [nick, setNickname] = useState(props.integration.name);
|
||||
const [picture, setPicture] = useState(props.integration.picture);
|
||||
const [picture, setPicture] = useState(props.integration.picture || '/no-picture.jpg');
|
||||
|
||||
const fetch = useFetch();
|
||||
const submitForm: FormEventHandler<HTMLFormElement> = useCallback(
|
||||
|
|
|
|||
|
|
@ -634,7 +634,7 @@ export const CalendarColumn: FC<{
|
|||
)}
|
||||
>
|
||||
<Image
|
||||
src={selectedIntegrations.picture}
|
||||
src={selectedIntegrations.picture || '/no-picture.jpg'}
|
||||
className="rounded-full"
|
||||
alt={selectedIntegrations.identifier}
|
||||
width={32}
|
||||
|
|
@ -747,7 +747,7 @@ const CalendarItem: FC<{
|
|||
>
|
||||
<img
|
||||
className="w-[20px] h-[20px] rounded-full"
|
||||
src={post.integration.picture!}
|
||||
src={post.integration.picture! || '/no-picture.jpg'}
|
||||
/>
|
||||
<img
|
||||
className="w-[12px] h-[12px] rounded-full absolute z-10 top-[10px] right-0 border border-fifth"
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export const GeneralPreviewComponent: FC<{maximumCharacters?: number}> = (props)
|
|||
>
|
||||
<div className="w-[40px] flex flex-col items-center">
|
||||
<img
|
||||
src={integration?.picture}
|
||||
src={integration?.picture || '/no-picture.jpg'}
|
||||
alt="x"
|
||||
className="rounded-full relative z-[2]"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ export const PickPlatforms: FC<{
|
|||
)}
|
||||
>
|
||||
<Image
|
||||
src={integration.picture}
|
||||
src={integration.picture || '/no-picture.jpg'}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={32}
|
||||
|
|
@ -302,7 +302,7 @@ export const PickPlatforms: FC<{
|
|||
<div className="flex items-center justify-center gap-[10px]">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={integration.picture}
|
||||
src={integration.picture || '/no-picture.jpg'}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={24}
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ export const MenuComponent: FC<
|
|||
)}
|
||||
<ImageWithFallback
|
||||
fallbackSrc={`/icons/platforms/${integration.identifier}.png`}
|
||||
src={integration.picture!}
|
||||
src={integration.picture || '/no-picture.jpg'}
|
||||
className="rounded-full"
|
||||
alt={integration.identifier}
|
||||
width={32}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
import { FC, MouseEventHandler, useCallback, useMemo, useState } from 'react';
|
||||
import React, {
|
||||
FC,
|
||||
MouseEventHandler,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useClickOutside } from '@mantine/hooks';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
|
|
@ -11,6 +17,9 @@ import { BotPicture } from '@gitroom/frontend/components/launches/bot.picture';
|
|||
import { CustomerModal } from '@gitroom/frontend/components/launches/customer.modal';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { SettingsModal } from '@gitroom/frontend/components/launches/settings.modal';
|
||||
import { string } from 'yup';
|
||||
import { CustomVariables } from '@gitroom/frontend/components/launches/add.provider.component';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export const Menu: FC<{
|
||||
canEnable: boolean;
|
||||
|
|
@ -35,6 +44,7 @@ export const Menu: FC<{
|
|||
refreshChannel,
|
||||
} = props;
|
||||
const fetch = useFetch();
|
||||
const router = useRouter();
|
||||
const { integrations } = useCalendar();
|
||||
const toast = useToaster();
|
||||
const modal = useModals();
|
||||
|
|
@ -209,6 +219,23 @@ export const Menu: FC<{
|
|||
setShow(false);
|
||||
}, [integrations]);
|
||||
|
||||
const updateCredentials = useCallback(() => {
|
||||
modal.openModal({
|
||||
title: '',
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'bg-transparent text-textColor',
|
||||
},
|
||||
children: (
|
||||
<CustomVariables
|
||||
identifier={findIntegration.identifier}
|
||||
gotoUrl={(url: string) => router.push(url)}
|
||||
variables={findIntegration.customFields}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="cursor-pointer relative select-none"
|
||||
|
|
@ -232,10 +259,34 @@ export const Menu: FC<{
|
|||
onClick={(e) => e.stopPropagation()}
|
||||
className={`absolute top-[100%] left-0 p-[8px] px-[20px] bg-fifth flex flex-col gap-[16px] z-[100] rounded-[8px] border border-tableBorder ${interClass} text-nowrap`}
|
||||
>
|
||||
{canDisable && findIntegration?.refreshNeeded && (
|
||||
{canDisable &&
|
||||
findIntegration?.refreshNeeded &&
|
||||
!findIntegration.customFields && (
|
||||
<div
|
||||
className="flex gap-[12px] items-center"
|
||||
onClick={refreshChannel(findIntegration!)}
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
width={18}
|
||||
height={18}
|
||||
viewBox="0 0 32 32"
|
||||
fill="yellow"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M3.00079 15.9999C3.00343 13.6138 3.95249 11.3262 5.63975 9.63891C7.327 7.95165 9.61465 7.00259 12.0008 6.99995H25.587L24.2933 5.70745C24.1056 5.5198 24.0002 5.26531 24.0002 4.99995C24.0002 4.73458 24.1056 4.48009 24.2933 4.29245C24.4809 4.1048 24.7354 3.99939 25.0008 3.99939C25.2661 3.99939 25.5206 4.10481 25.7083 4.29245L28.7083 7.29245C28.8013 7.38532 28.875 7.49561 28.9253 7.61701C28.9757 7.7384 29.0016 7.86853 29.0016 7.99995C29.0016 8.13136 28.9757 8.26149 28.9253 8.38289C28.875 8.50428 28.8013 8.61457 28.7083 8.70745L25.7083 11.7074C25.5206 11.8951 25.2661 12.0005 25.0008 12.0005C24.7354 12.0005 24.4809 11.8951 24.2933 11.7074C24.1056 11.5198 24.0002 11.2653 24.0002 10.9999C24.0002 10.7346 24.1056 10.4801 24.2933 10.2924L25.587 8.99995H12.0008C10.1449 9.00193 8.36556 9.74007 7.05323 11.0524C5.74091 12.3647 5.00277 14.144 5.00079 15.9999C5.00079 16.2652 4.89543 16.5195 4.70789 16.7071C4.52036 16.8946 4.266 16.9999 4.00079 16.9999C3.73557 16.9999 3.48122 16.8946 3.29368 16.7071C3.10614 16.5195 3.00079 16.2652 3.00079 15.9999ZM28.0008 14.9999C27.7356 14.9999 27.4812 15.1053 27.2937 15.2928C27.1061 15.4804 27.0008 15.7347 27.0008 15.9999C26.9988 17.8559 26.2607 19.6352 24.9483 20.9475C23.636 22.2598 21.8567 22.998 20.0008 22.9999H6.41454L7.70829 21.7074C7.8012 21.6145 7.8749 21.5042 7.92518 21.3828C7.97546 21.2614 8.00134 21.1313 8.00134 20.9999C8.00134 20.8686 7.97546 20.7384 7.92518 20.6171C7.8749 20.4957 7.8012 20.3854 7.70829 20.2924C7.61538 20.1995 7.50508 20.1258 7.38368 20.0756C7.26229 20.0253 7.13218 19.9994 7.00079 19.9994C6.86939 19.9994 6.73928 20.0253 6.61789 20.0756C6.4965 20.1258 6.3862 20.1995 6.29329 20.2924L3.29329 23.2924C3.20031 23.3853 3.12655 23.4956 3.07623 23.617C3.0259 23.7384 3 23.8685 3 23.9999C3 24.1314 3.0259 24.2615 3.07623 24.3829C3.12655 24.5043 3.20031 24.6146 3.29329 24.7074L6.29329 27.7074C6.3862 27.8004 6.4965 27.8741 6.61789 27.9243C6.73928 27.9746 6.86939 28.0005 7.00079 28.0005C7.13218 28.0005 7.26229 27.9746 7.38368 27.9243C7.50508 27.8741 7.61538 27.8004 7.70829 27.7074C7.8012 27.6145 7.8749 27.5042 7.92518 27.3828C7.97546 27.2614 8.00134 27.1313 8.00134 26.9999C8.00134 26.8686 7.97546 26.7384 7.92518 26.6171C7.8749 26.4957 7.8012 26.3854 7.70829 26.2924L6.41454 24.9999H20.0008C22.3869 24.9973 24.6746 24.0482 26.3618 22.361C28.0491 20.6737 28.9981 18.3861 29.0008 15.9999C29.0008 15.7347 28.8954 15.4804 28.7079 15.2928C28.5204 15.1053 28.266 14.9999 28.0008 14.9999Z"
|
||||
fill="yellow"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px]">Reconnect channel</div>
|
||||
</div>
|
||||
)}
|
||||
{!!findIntegration?.isCustomFields && (
|
||||
<div
|
||||
className="flex gap-[12px] items-center"
|
||||
onClick={refreshChannel(findIntegration!)}
|
||||
onClick={updateCredentials}
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
|
|
@ -251,7 +302,7 @@ export const Menu: FC<{
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-[12px]">Reconnect channel</div>
|
||||
<div className="text-[12px]">Update Credentials</div>
|
||||
</div>
|
||||
)}
|
||||
{findIntegration?.additionalSettings !== '[]' && (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
|
||||
export default withProvider(
|
||||
null,
|
||||
undefined,
|
||||
undefined,
|
||||
async () => {
|
||||
return true;
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
|
@ -19,6 +19,8 @@ import MastodonProvider from '@gitroom/frontend/components/launches/providers/ma
|
|||
import BlueskyProvider from '@gitroom/frontend/components/launches/providers/bluesky/bluesky.provider';
|
||||
import LemmyProvider from '@gitroom/frontend/components/launches/providers/lemmy/lemmy.provider';
|
||||
import WarpcastProvider from '@gitroom/frontend/components/launches/providers/warpcast/warpcast.provider';
|
||||
import TelegramProvider from '@gitroom/frontend/components/launches/providers/telegram/telegram.provider';
|
||||
import NostrProvider from '@gitroom/frontend/components/launches/providers/nostr/nostr.provider';
|
||||
|
||||
export const Providers = [
|
||||
{identifier: 'devto', component: DevtoProvider},
|
||||
|
|
@ -42,6 +44,8 @@ export const Providers = [
|
|||
{identifier: 'bluesky', component: BlueskyProvider},
|
||||
{identifier: 'lemmy', component: LemmyProvider},
|
||||
{identifier: 'wrapcast', component: WarpcastProvider},
|
||||
{identifier: 'telegram', component: TelegramProvider},
|
||||
{identifier: 'nostr', component: NostrProvider},
|
||||
];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
import { withProvider } from '@gitroom/frontend/components/launches/providers/high.order.provider';
|
||||
|
||||
export default withProvider(
|
||||
null,
|
||||
undefined,
|
||||
undefined,
|
||||
async () => {
|
||||
return true;
|
||||
},
|
||||
500
|
||||
);
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
'use client';
|
||||
import '@neynar/react/dist/style.css';
|
||||
import React, { FC, useMemo, useState, useCallback, useEffect } from 'react';
|
||||
import { Web3ProviderInterface } from '@gitroom/frontend/components/launches/web3/web3.provider.interface';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
|
||||
import {
|
||||
NeynarAuthButton,
|
||||
NeynarContextProvider,
|
||||
Theme,
|
||||
useNeynarContext,
|
||||
} from '@neynar/react';
|
||||
import { INeynarAuthenticatedUser } from '@neynar/react/dist/types/common';
|
||||
import { ButtonCaster } from '@gitroom/frontend/components/auth/providers/farcaster.provider';
|
||||
|
||||
export const WrapcasterProvider: FC<Web3ProviderInterface> = (props) => {
|
||||
const [_, state] = props.nonce.split('||');
|
||||
const modal = useModals();
|
||||
const [hide, setHide] = useState(false);
|
||||
|
||||
const auth = useCallback((code: string) => {
|
||||
setHide(true);
|
||||
return props.onComplete(code, state);
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<div className="rounded-[4px] border border-customColor6 bg-sixth px-[16px] pb-[16px] relative w-full">
|
||||
<TopTitle title={`Add Wrapcast`} />
|
||||
<button
|
||||
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
onClick={() => modal.closeAll()}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="justify-center items-center flex">
|
||||
{hide ? (
|
||||
<div className="justify-center items-center flex -mt-[90px]">
|
||||
<LoadingComponent width={100} height={100} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="justify-center items-center py-[20px] flex-col w-[500px]">
|
||||
<ButtonCaster login={auth} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
'use client';
|
||||
import '@neynar/react/dist/style.css';
|
||||
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Web3ProviderInterface } from '@gitroom/frontend/components/launches/web3/web3.provider.interface';
|
||||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { useToaster } from '@gitroom/react/toaster/toaster';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
|
||||
export const TelegramProvider: FC<Web3ProviderInterface> = (props) => {
|
||||
const { onComplete, nonce } = props;
|
||||
const {telegramBotName} = useVariables();
|
||||
const modal = useModals();
|
||||
const fetch = useFetch();
|
||||
const word = useRef(makeId(4));
|
||||
const stop = useRef(false);
|
||||
const [step, setStep] = useState(false);
|
||||
const toaster = useToaster();
|
||||
|
||||
async function* load() {
|
||||
let id = '';
|
||||
while (true) {
|
||||
const data = await (
|
||||
await fetch(
|
||||
`/integrations/telegram/updates?word=${word.current}${
|
||||
id ? `&id=${id}` : ''
|
||||
}`
|
||||
)
|
||||
).json();
|
||||
|
||||
if (data.lastChatId) {
|
||||
id = data.lastChatId;
|
||||
}
|
||||
|
||||
yield data;
|
||||
}
|
||||
}
|
||||
|
||||
const loadAll = async () => {
|
||||
stop.current = false;
|
||||
setStep(true);
|
||||
const generator = load();
|
||||
for await (const data of generator) {
|
||||
if (stop.current) {
|
||||
return;
|
||||
}
|
||||
if (data.chatId) {
|
||||
onComplete(data.chatId, nonce);
|
||||
return;
|
||||
}
|
||||
await timer(2000);
|
||||
}
|
||||
};
|
||||
|
||||
const copyText = useCallback(() => {
|
||||
copy(`/connect ${word.current}`);
|
||||
toaster.show('Copied to clipboard', 'success');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
stop.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="rounded-[4px] border border-customColor6 bg-sixth px-[16px] pb-[16px] relative w-[700px]">
|
||||
<TopTitle title={`Add Telegram`} />
|
||||
<button
|
||||
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
onClick={() => modal.closeAll()}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="justify-center items-center flex flex-col pt-[16px]">
|
||||
<div>
|
||||
Please add <strong>@{telegramBotName}</strong> to your
|
||||
telegram group / channel and click here:
|
||||
</div>
|
||||
{!step ? (
|
||||
<div className="w-full mt-[16px]" onClick={loadAll}>
|
||||
<div
|
||||
className={`cursor-pointer bg-[#2EA6DD] h-[44px] rounded-[4px] flex justify-center items-center text-white ${interClass} gap-[4px]`}
|
||||
>
|
||||
<svg
|
||||
width="51"
|
||||
height="22"
|
||||
viewBox="0 0 72 63"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M71.85 3.00001L60.612 60.378C60.612 60.378 60.129 63 56.877 63C55.149 63 54.258 62.178 54.258 62.178L29.916 41.979L18.006 35.976L2.721 31.911C2.721 31.911 0 31.125 0 28.875C0 27 2.799 26.106 2.799 26.106L66.747 0.70201C66.747 0.70201 68.7 -0.00299041 70.125 9.58803e-06C71.001 9.58803e-06 72 0.37501 72 1.50001C72 2.25001 71.85 3.00001 71.85 3.00001Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M39.0005 49.5147L28.7225 59.6367C28.7225 59.6367 28.2755 59.9817 27.6785 59.9967C27.4715 60.0027 27.2495 59.9697 27.0215 59.8677L29.9135 41.9727L39.0005 49.5147Z"
|
||||
fill="#B0BEC5"
|
||||
/>
|
||||
<path
|
||||
d="M59.691 12.5877C59.184 11.9277 58.248 11.8077 57.588 12.3087L18 35.9997C18 35.9997 24.318 53.6757 25.281 56.7357C26.247 59.7987 27.021 59.8707 27.021 59.8707L29.913 41.9757L59.409 14.6877C60.069 14.1867 60.192 13.2477 59.691 12.5877Z"
|
||||
fill="#CFD8DC"
|
||||
/>
|
||||
</svg>
|
||||
<div>Connect Telegram</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full text-center" onClick={copyText}>
|
||||
Please add the following command in your chat:
|
||||
<div className="mt-[16px] flex">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label=""
|
||||
value={`/connect ${word.current}`}
|
||||
name=""
|
||||
disableForm={true}
|
||||
/>
|
||||
</div>
|
||||
<Button>Copy</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -6,76 +6,59 @@ import { useVariables } from '@gitroom/react/helpers/variable.context';
|
|||
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
|
||||
import { NeynarAuthButton, NeynarContextProvider, Theme, useNeynarContext } from '@neynar/react';
|
||||
import {
|
||||
NeynarAuthButton,
|
||||
NeynarContextProvider,
|
||||
Theme,
|
||||
useNeynarContext,
|
||||
} from '@neynar/react';
|
||||
import { INeynarAuthenticatedUser } from '@neynar/react/dist/types/common';
|
||||
import { ButtonCaster } from '@gitroom/frontend/components/auth/providers/farcaster.provider';
|
||||
|
||||
export const WrapcasterProvider: FC<Web3ProviderInterface> = (props) => {
|
||||
const [id, state] = props.nonce.split('||');
|
||||
const [_, state] = props.nonce.split('||');
|
||||
const modal = useModals();
|
||||
const [hide, setHide] = useState(false);
|
||||
|
||||
const auth = useCallback((params: { user: INeynarAuthenticatedUser }) => {
|
||||
const auth = useCallback((code: string) => {
|
||||
setHide(true);
|
||||
return props.onComplete(Buffer.from(JSON.stringify(params.user)).toString('base64'), state);
|
||||
}, []);
|
||||
return props.onComplete(code, state);
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
<NeynarContextProvider
|
||||
settings={{
|
||||
clientId: id || '',
|
||||
defaultTheme: Theme.Dark,
|
||||
// eventsCallbacks: {
|
||||
// onAuthSuccess: (params: { user: INeynarAuthenticatedUser }) => {
|
||||
// auth(params);
|
||||
// },
|
||||
// },
|
||||
}}
|
||||
>
|
||||
<div className="rounded-[4px] border border-customColor6 bg-sixth px-[16px] pb-[16px] relative w-full">
|
||||
<TopTitle title={`Add Wrapcast`} />
|
||||
<button
|
||||
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
onClick={() => modal.closeAll()}
|
||||
<div className="rounded-[4px] border border-customColor6 bg-sixth px-[16px] pb-[16px] relative w-full">
|
||||
<TopTitle title={`Add Wrapcast`} />
|
||||
<button
|
||||
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
type="button"
|
||||
onClick={() => modal.closeAll()}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="justify-center items-center flex">
|
||||
{hide ? (
|
||||
<div className="justify-center items-center flex -mt-[90px]">
|
||||
<LoadingComponent width={100} height={100} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="justify-center items-center flex py-[20px]">
|
||||
<Logged onSuccess={auth} />
|
||||
<NeynarAuthButton className="right-4 top-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<path
|
||||
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div className="justify-center items-center flex">
|
||||
{hide ? (
|
||||
<div className="justify-center items-center flex -mt-[90px]">
|
||||
<LoadingComponent width={100} height={100} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="justify-center items-center py-[20px] flex-col w-[500px]">
|
||||
<ButtonCaster login={auth} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</NeynarContextProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Logged: FC<{onSuccess: (params: {user: INeynarAuthenticatedUser}) => void}> = (props) => {
|
||||
const context = useNeynarContext();
|
||||
useEffect(() => {
|
||||
if (context.isAuthenticated && context.user) {
|
||||
props.onSuccess({ user: context.user });
|
||||
}
|
||||
}, [context.isAuthenticated]);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
import { FC } from 'react';
|
||||
import { Web3ProviderInterface } from '@gitroom/frontend/components/launches/web3/web3.provider.interface';
|
||||
import { WrapcasterProvider } from '@gitroom/frontend/components/launches/web3/providers/wrapcaster.provider';
|
||||
import { TelegramProvider } from '@gitroom/frontend/components/launches/web3/providers/telegram.provider';
|
||||
|
||||
export const web3List: {
|
||||
identifier: string;
|
||||
component: FC<Web3ProviderInterface>;
|
||||
}[] = [
|
||||
{
|
||||
identifier: 'telegram',
|
||||
component: TelegramProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'wrapcast',
|
||||
component: WrapcasterProvider,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
|||
import { Select } from '@gitroom/react/form/select';
|
||||
import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { setCookie } from '@gitroom/frontend/components/layout/layout.context';
|
||||
|
||||
export const Subscription = () => {
|
||||
const fetch = useFetch();
|
||||
|
|
@ -53,6 +55,7 @@ export const Subscription = () => {
|
|||
export const Impersonate = () => {
|
||||
const fetch = useFetch();
|
||||
const [name, setName] = useState('');
|
||||
const { isSecured } = useVariables();
|
||||
const user = useUser();
|
||||
|
||||
const load = useCallback(async () => {
|
||||
|
|
@ -65,10 +68,14 @@ export const Impersonate = () => {
|
|||
}, [name]);
|
||||
|
||||
const stopImpersonating = useCallback(async () => {
|
||||
await fetch(`/user/impersonate`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ id: '' }),
|
||||
});
|
||||
if (!isSecured) {
|
||||
setCookie('impersonate', '', -10);
|
||||
} else {
|
||||
await fetch(`/user/impersonate`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ id: '' }),
|
||||
});
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -14,13 +14,57 @@ export default function LayoutContext(params: { children: ReactNode }) {
|
|||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export function setCookie(cname: string, cvalue: string, exdays: number) {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const d = new Date();
|
||||
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000);
|
||||
const expires = 'expires=' + d.toUTCString();
|
||||
document.cookie = cname + '=' + cvalue + ';' + expires + ';path=/';
|
||||
}
|
||||
|
||||
function LayoutContextInner(params: { children: ReactNode }) {
|
||||
const returnUrl = useReturnUrl();
|
||||
const {backendUrl, isGeneral} = useVariables();
|
||||
const { backendUrl, isGeneral, isSecured } = useVariables();
|
||||
|
||||
const afterRequest = useCallback(
|
||||
async (url: string, options: RequestInit, response: Response) => {
|
||||
if (typeof window !== 'undefined' && window.location.href.includes('/p/')) {
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
window.location.href.includes('/p/')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const headerAuth =
|
||||
response?.headers?.get('auth') || response?.headers?.get('Auth');
|
||||
const showOrg =
|
||||
response?.headers?.get('showorg') || response?.headers?.get('Showorg');
|
||||
const impersonate =
|
||||
response?.headers?.get('impersonate') ||
|
||||
response?.headers?.get('Impersonate');
|
||||
const logout =
|
||||
response?.headers?.get('logout') || response?.headers?.get('Logout');
|
||||
|
||||
if (headerAuth) {
|
||||
setCookie('auth', headerAuth, 365);
|
||||
}
|
||||
|
||||
if (showOrg) {
|
||||
setCookie('showorg', showOrg, 365);
|
||||
}
|
||||
|
||||
if (impersonate) {
|
||||
setCookie('impersonate', impersonate, 365);
|
||||
}
|
||||
|
||||
if (logout && !isSecured) {
|
||||
setCookie('auth', '', -10);
|
||||
setCookie('showorg', '', -10);
|
||||
setCookie('impersonate', '', -10);
|
||||
window.location.href = '/';
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -50,6 +94,11 @@ function LayoutContextInner(params: { children: ReactNode }) {
|
|||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
if (!isSecured) {
|
||||
setCookie('auth', '', -10);
|
||||
setCookie('showorg', '', -10);
|
||||
setCookie('impersonate', '', -10);
|
||||
}
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
|
|
@ -74,10 +123,7 @@ function LayoutContextInner(params: { children: ReactNode }) {
|
|||
);
|
||||
|
||||
return (
|
||||
<FetchWrapperComponent
|
||||
baseUrl={backendUrl}
|
||||
afterRequest={afterRequest}
|
||||
>
|
||||
<FetchWrapperComponent baseUrl={backendUrl} afterRequest={afterRequest}>
|
||||
{params?.children || <></>}
|
||||
</FetchWrapperComponent>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,19 +4,28 @@ import { useCallback } from 'react';
|
|||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { setCookie } from '@gitroom/frontend/components/layout/layout.context';
|
||||
|
||||
export const LogoutComponent = () => {
|
||||
const fetch = useFetch();
|
||||
const {isGeneral} = useVariables();
|
||||
const { isGeneral, isSecured } = useVariables();
|
||||
const logout = useCallback(async () => {
|
||||
if (await deleteDialog('Are you sure you want to logout?', 'Yes logout')) {
|
||||
await fetch('/user/logout', {
|
||||
method: 'POST',
|
||||
});
|
||||
if (!isSecured) {
|
||||
setCookie('auth', '', -10);
|
||||
} else {
|
||||
await fetch('/user/logout', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
window.location.href = '/';
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <div className="text-red-400 cursor-pointer" onClick={logout}>Logout from {isGeneral ? 'Postiz' : 'Gitroom'}</div>;
|
||||
return (
|
||||
<div className="text-red-400 cursor-pointer" onClick={logout}>
|
||||
Logout from {isGeneral ? 'Postiz' : 'Gitroom'}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import { internalFetch } from '@gitroom/helpers/utils/internal.fetch';
|
|||
export async function middleware(request: NextRequest) {
|
||||
const nextUrl = request.nextUrl;
|
||||
const authCookie = request.cookies.get('auth');
|
||||
if (nextUrl.pathname.startsWith('/uploads/') || nextUrl.pathname.startsWith('/p/') || nextUrl.pathname.startsWith('/icons/')) {
|
||||
if (
|
||||
nextUrl.pathname.startsWith('/uploads/') ||
|
||||
nextUrl.pathname.startsWith('/p/') ||
|
||||
nextUrl.pathname.startsWith('/icons/')
|
||||
) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
// If the URL is logout, delete the cookie and redirect to login
|
||||
|
|
@ -17,9 +21,13 @@ export async function middleware(request: NextRequest) {
|
|||
);
|
||||
response.cookies.set('auth', '', {
|
||||
path: '/',
|
||||
sameSite: false,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: false,
|
||||
}
|
||||
: {}),
|
||||
maxAge: -1,
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
});
|
||||
|
|
@ -53,12 +61,16 @@ export async function middleware(request: NextRequest) {
|
|||
if (org) {
|
||||
const redirect = NextResponse.redirect(new URL(`/`, nextUrl.href));
|
||||
redirect.cookies.set('org', org, {
|
||||
path: '/',
|
||||
sameSite: false,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
path: '/',
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: false,
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
}
|
||||
: {}),
|
||||
expires: new Date(Date.now() + 15 * 60 * 1000),
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
});
|
||||
return redirect;
|
||||
}
|
||||
|
|
@ -81,12 +93,16 @@ export async function middleware(request: NextRequest) {
|
|||
);
|
||||
if (id) {
|
||||
redirect.cookies.set('showorg', id, {
|
||||
path: '/',
|
||||
sameSite: false,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
path: '/',
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: false,
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
}
|
||||
: {}),
|
||||
expires: new Date(Date.now() + 15 * 60 * 1000),
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -112,9 +128,13 @@ export async function middleware(request: NextRequest) {
|
|||
|
||||
next.cookies.set('marketplace', type === 'seller' ? 'seller' : 'buyer', {
|
||||
path: '/',
|
||||
sameSite: false,
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
...(!process.env.NOT_SECURED
|
||||
? {
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
sameSite: false,
|
||||
}
|
||||
: {}),
|
||||
expires: new Date(Date.now() + 15 * 60 * 1000),
|
||||
domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!),
|
||||
});
|
||||
|
|
@ -122,6 +142,7 @@ export async function middleware(request: NextRequest) {
|
|||
|
||||
return next;
|
||||
} catch (err) {
|
||||
console.log('err', err);
|
||||
return NextResponse.redirect(new URL('/auth/logout', nextUrl.href));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
export interface Params {
|
||||
baseUrl: string;
|
||||
beforeRequest?: (url: string, options: RequestInit) => Promise<RequestInit>;
|
||||
|
|
@ -11,21 +10,48 @@ export interface Params {
|
|||
export const customFetch = (
|
||||
params: Params,
|
||||
auth?: string,
|
||||
showorg?: string
|
||||
showorg?: string,
|
||||
secured: boolean = true
|
||||
) => {
|
||||
return async function newFetch(url: string, options: RequestInit = {}) {
|
||||
const newRequestObject = await params?.beforeRequest?.(url, options);
|
||||
const authNonSecuredCookie = typeof document === 'undefined' ? null : document.cookie
|
||||
.split(';')
|
||||
.find((p) => p.includes('auth='))
|
||||
?.split('=')[1];
|
||||
|
||||
const authNonSecuredOrg = typeof document === 'undefined' ? null : document.cookie
|
||||
.split(';')
|
||||
.find((p) => p.includes('showorg='))
|
||||
?.split('=')[1];
|
||||
|
||||
const authNonSecuredImpersonate = typeof document === 'undefined' ? null : document.cookie
|
||||
.split(';')
|
||||
.find((p) => p.includes('impersonate='))
|
||||
?.split('=')[1];
|
||||
|
||||
const fetchRequest = await fetch(params.baseUrl + url, {
|
||||
credentials: 'include',
|
||||
...(secured ? { credentials: 'include' } : {}),
|
||||
...(newRequestObject || options),
|
||||
headers: {
|
||||
...(auth ? { auth } : {}),
|
||||
...(showorg ? { showorg } : {}),
|
||||
...(showorg
|
||||
? { showorg }
|
||||
: authNonSecuredOrg
|
||||
? { showorg: authNonSecuredOrg }
|
||||
: {}),
|
||||
...(options.body instanceof FormData
|
||||
? {}
|
||||
: { 'Content-Type': 'application/json' }),
|
||||
Accept: 'application/json',
|
||||
...options?.headers,
|
||||
...(auth
|
||||
? { auth }
|
||||
: authNonSecuredCookie
|
||||
? { auth: authNonSecuredCookie }
|
||||
: {}),
|
||||
...(authNonSecuredImpersonate
|
||||
? { impersonate: authNonSecuredImpersonate }
|
||||
: {}),
|
||||
},
|
||||
// @ts-ignore
|
||||
...(!options.next && options.cache !== 'force-cache'
|
||||
|
|
|
|||
|
|
@ -1,30 +1,46 @@
|
|||
"use client";
|
||||
'use client';
|
||||
|
||||
import {createContext, FC, ReactNode, useContext, useRef, useState} from "react";
|
||||
import {customFetch, Params} from "./custom.fetch.func";
|
||||
import {
|
||||
createContext,
|
||||
FC,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { customFetch, Params } from './custom.fetch.func';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
|
||||
const FetchProvider = createContext(customFetch(
|
||||
const FetchProvider = createContext(
|
||||
customFetch(
|
||||
// @ts-ignore
|
||||
{
|
||||
baseUrl: '',
|
||||
beforeRequest: () => {},
|
||||
afterRequest: () => {
|
||||
return true;
|
||||
}
|
||||
} as Params));
|
||||
baseUrl: '',
|
||||
beforeRequest: () => {},
|
||||
afterRequest: () => {
|
||||
return true;
|
||||
},
|
||||
} as Params
|
||||
)
|
||||
);
|
||||
|
||||
export const FetchWrapperComponent: FC<Params & {children: ReactNode}> = (props) => {
|
||||
const {children, ...params} = props;
|
||||
export const FetchWrapperComponent: FC<Params & { children: ReactNode }> = (
|
||||
props
|
||||
) => {
|
||||
const { children, ...params } = props;
|
||||
const { isSecured } = useVariables();
|
||||
// @ts-ignore
|
||||
const fetchData = useRef(
|
||||
customFetch(params, undefined, undefined, isSecured)
|
||||
);
|
||||
return (
|
||||
// @ts-ignore
|
||||
const fetchData = useRef(customFetch(params));
|
||||
return (
|
||||
// @ts-ignore
|
||||
<FetchProvider.Provider value={fetchData.current}>
|
||||
{children}
|
||||
</FetchProvider.Provider>
|
||||
)
|
||||
}
|
||||
<FetchProvider.Provider value={fetchData.current}>
|
||||
{children}
|
||||
</FetchProvider.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useFetch = () => {
|
||||
return useContext(FetchProvider);
|
||||
}
|
||||
return useContext(FetchProvider);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -170,6 +170,10 @@ export class IntegrationRepository {
|
|||
: '[]',
|
||||
},
|
||||
update: {
|
||||
...(additionalSettings
|
||||
? { additionalSettings: JSON.stringify(additionalSettings) }
|
||||
: {}),
|
||||
...(customInstanceDetails ? { customInstanceDetails } : {}),
|
||||
type: type as any,
|
||||
...(!refresh
|
||||
? {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ export class OrganizationRepository {
|
|||
});
|
||||
}
|
||||
|
||||
getCount() {
|
||||
return this._organization.model.organization.count();
|
||||
}
|
||||
|
||||
getUserOrg(id: string) {
|
||||
return this._userOrg.model.userOrganization.findFirst({
|
||||
where: {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ export class OrganizationService {
|
|||
);
|
||||
}
|
||||
|
||||
async getCount() {
|
||||
return this._organizationRepository.getCount();
|
||||
}
|
||||
|
||||
addUserToOrg(
|
||||
userId: string,
|
||||
id: string,
|
||||
|
|
|
|||
|
|
@ -536,6 +536,7 @@ enum Provider {
|
|||
LOCAL
|
||||
GITHUB
|
||||
GOOGLE
|
||||
FARCASTER
|
||||
}
|
||||
|
||||
enum Role {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import { BlueskyProvider } from '@gitroom/nestjs-libraries/integrations/social/b
|
|||
import { LemmyProvider } from '@gitroom/nestjs-libraries/integrations/social/lemmy.provider';
|
||||
import { InstagramStandaloneProvider } from '@gitroom/nestjs-libraries/integrations/social/instagram.standalone.provider';
|
||||
import { FarcasterProvider } from '@gitroom/nestjs-libraries/integrations/social/farcaster.provider';
|
||||
import { TelegramProvider } from '@gitroom/nestjs-libraries/integrations/social/telegram.provider';
|
||||
import { NostrProvider } from '@gitroom/nestjs-libraries/integrations/social/nostr.provider';
|
||||
|
||||
const socialIntegrationList: SocialProvider[] = [
|
||||
new XProvider(),
|
||||
|
|
@ -44,6 +46,8 @@ const socialIntegrationList: SocialProvider[] = [
|
|||
new BlueskyProvider(),
|
||||
new LemmyProvider(),
|
||||
new FarcasterProvider(),
|
||||
new TelegramProvider(),
|
||||
new NostrProvider(),
|
||||
// new MastodonCustomProvider(),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ import {
|
|||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import {
|
||||
NotEnoughScopes,
|
||||
SocialAbstract,
|
||||
NotEnoughScopes, RefreshToken, SocialAbstract
|
||||
} from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { BskyAgent, RichText } from '@atproto/api';
|
||||
import dayjs from 'dayjs';
|
||||
|
|
@ -69,7 +68,7 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
{
|
||||
key: 'identifier',
|
||||
label: 'Identifier',
|
||||
validation: `/^.{3,}$/`,
|
||||
validation: `/^.+$/`,
|
||||
type: 'text' as const,
|
||||
},
|
||||
{
|
||||
|
|
@ -152,10 +151,14 @@ export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
|||
service: body.service,
|
||||
});
|
||||
|
||||
await agent.login({
|
||||
identifier: body.identifier,
|
||||
password: body.password,
|
||||
});
|
||||
try {
|
||||
await agent.login({
|
||||
identifier: body.identifier,
|
||||
password: body.password,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new RefreshToken('bluesky', JSON.stringify(err), {} as BodyInit);
|
||||
}
|
||||
|
||||
let loadCid = '';
|
||||
let loadUri = '';
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export class DiscordProvider extends SocialAbstract implements SocialProvider {
|
|||
).json();
|
||||
|
||||
return list
|
||||
.filter((p: any) => p.type === 0 || p.type === 15)
|
||||
.filter((p: any) => p.type === 0 || p.type === 5 || p.type === 15)
|
||||
.map((p: any) => ({
|
||||
id: String(p.id),
|
||||
name: p.name,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
import {
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import dayjs from 'dayjs';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { getPublicKey, Relay, finalizeEvent } from 'nostr-tools';
|
||||
import WebSocket from 'ws';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
|
||||
// @ts-ignore
|
||||
global.WebSocket = WebSocket;
|
||||
|
||||
const list = [
|
||||
'wss://relay.primal.net',
|
||||
'wss://relay.damus.io',
|
||||
'wss://relay.snort.social',
|
||||
'wss://nostr.wine',
|
||||
'wss://nos.lol',
|
||||
'wss://relay.primal.net',
|
||||
];
|
||||
|
||||
export class NostrProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'nostr';
|
||||
name = 'Nostr';
|
||||
isBetweenSteps = false;
|
||||
scopes = [];
|
||||
|
||||
async customFields() {
|
||||
return [
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Nostr private key',
|
||||
validation: `/^.{3,}$/`,
|
||||
type: 'password' as const,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
|
||||
return {
|
||||
refreshToken: '',
|
||||
expiresIn: 0,
|
||||
accessToken: '',
|
||||
id: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(17);
|
||||
return {
|
||||
url: '',
|
||||
codeVerifier: makeId(10),
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
private async findRelayInformation(pubkey: string) {
|
||||
for (const relay of list) {
|
||||
const relayInstance = await Relay.connect(relay);
|
||||
const value = await new Promise<any>((resolve) => {
|
||||
console.log('connecting');
|
||||
relayInstance.subscribe([{ kinds: [0], authors: [pubkey] }], {
|
||||
eoseTimeout: 6000,
|
||||
onevent: (event) => {
|
||||
resolve(event);
|
||||
},
|
||||
oneose: () => {
|
||||
resolve({});
|
||||
},
|
||||
onclose: () => {
|
||||
resolve({});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
relayInstance.close();
|
||||
const content = JSON.parse(value?.content || '{}');
|
||||
if (content.name || content.displayName || content.display_name) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private async publish(pubkey: string, event: any) {
|
||||
let id = '';
|
||||
for (const relay of list) {
|
||||
try {
|
||||
const relayInstance = await Relay.connect(relay);
|
||||
const value = new Promise<any>((resolve) => {
|
||||
relayInstance.subscribe([{ kinds: [1], authors: [pubkey] }], {
|
||||
eoseTimeout: 6000,
|
||||
onevent: (event) => {
|
||||
resolve(event);
|
||||
},
|
||||
oneose: () => {
|
||||
resolve({});
|
||||
},
|
||||
onclose: () => {
|
||||
resolve({});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await relayInstance.publish(event);
|
||||
const all = await value;
|
||||
relayInstance.close();
|
||||
// relayInstance.close();
|
||||
id = id || all?.id;
|
||||
} catch (err) {
|
||||
/**empty**/
|
||||
}
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async authenticate(params: {
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
refresh?: string;
|
||||
}) {
|
||||
try {
|
||||
const body = JSON.parse(Buffer.from(params.code, 'base64').toString());
|
||||
|
||||
const pubkey = getPublicKey(
|
||||
Uint8Array.from(
|
||||
body.password.match(/.{1,2}/g).map((byte: any) => parseInt(byte, 16))
|
||||
)
|
||||
);
|
||||
|
||||
const user = await this.findRelayInformation(pubkey);
|
||||
|
||||
return {
|
||||
id: String(user.pubkey),
|
||||
name: user.display_name || user.displayName || 'No Name',
|
||||
accessToken: AuthService.signJWT({ password: body.password }),
|
||||
refreshToken: '',
|
||||
expiresIn: dayjs().add(200, 'year').unix() - dayjs().unix(),
|
||||
picture: user.picture,
|
||||
username: user.name || 'nousername',
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return 'Invalid credentials';
|
||||
}
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[]
|
||||
): Promise<PostResponse[]> {
|
||||
const { password } = AuthService.verifyJWT(accessToken) as any;
|
||||
|
||||
let lastId = '';
|
||||
const ids: PostResponse[] = [];
|
||||
for (const post of postDetails) {
|
||||
const textEvent = finalizeEvent(
|
||||
{
|
||||
kind: 1, // Text note
|
||||
content:
|
||||
post.message + '\n\n' + post.media?.map((m) => m.url).join('\n\n'),
|
||||
tags: [
|
||||
...(lastId
|
||||
? [
|
||||
['e', lastId, '', 'reply'],
|
||||
['p', id],
|
||||
]
|
||||
: []),
|
||||
], // Include delegation token in the event
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
},
|
||||
password
|
||||
);
|
||||
|
||||
lastId = await this.publish(id, textEvent);
|
||||
ids.push({
|
||||
id: post.id,
|
||||
postId: String(lastId),
|
||||
releaseURL: `https://primal.net/e/${lastId}`,
|
||||
status: 'completed',
|
||||
});
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import {
|
||||
AuthTokenDetails,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
SocialProvider,
|
||||
} from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import dayjs from 'dayjs';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import TelegramBot from 'node-telegram-bot-api';
|
||||
import { Integration } from '@prisma/client';
|
||||
|
||||
const telegramBot = new TelegramBot(process.env.TELEGRAM_TOKEN!);
|
||||
|
||||
export class TelegramProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'telegram';
|
||||
name = 'Telegram';
|
||||
isBetweenSteps = false;
|
||||
isWeb3 = true;
|
||||
scopes = [];
|
||||
|
||||
async refreshToken(refresh_token: string): Promise<AuthTokenDetails> {
|
||||
return {
|
||||
refreshToken: '',
|
||||
expiresIn: 0,
|
||||
accessToken: '',
|
||||
id: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(17);
|
||||
return {
|
||||
url: state,
|
||||
codeVerifier: makeId(10),
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
async authenticate(params: {
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
refresh?: string;
|
||||
}) {
|
||||
const chat = await telegramBot.getChat(params.code);
|
||||
|
||||
console.log(JSON.stringify(chat))
|
||||
if (!chat?.id) {
|
||||
return 'No chat found';
|
||||
}
|
||||
|
||||
const photo = !chat?.photo?.big_file_id
|
||||
? ''
|
||||
: await telegramBot.getFileLink(chat.photo.big_file_id);
|
||||
return {
|
||||
id: String(chat.username),
|
||||
name: chat.title!,
|
||||
accessToken: String(chat.id),
|
||||
refreshToken: '',
|
||||
expiresIn: dayjs().add(200, 'year').unix() - dayjs().unix(),
|
||||
picture: photo,
|
||||
username: chat.username!,
|
||||
};
|
||||
}
|
||||
|
||||
async getBotId(query: { id?: number; word: string }) {
|
||||
const res = await telegramBot.getUpdates({
|
||||
...(query.id ? { offset: query.id } : {}),
|
||||
});
|
||||
|
||||
const chatId = res?.find(
|
||||
(p) => p?.message?.text === `/connect ${query.word}`
|
||||
)?.message?.chat?.id;
|
||||
|
||||
return chatId
|
||||
? {
|
||||
chatId,
|
||||
}
|
||||
: res.length > 0
|
||||
? {
|
||||
lastChatId: res?.[res.length - 1]?.message?.chat?.id,
|
||||
}
|
||||
: {};
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[]
|
||||
): Promise<PostResponse[]> {
|
||||
const ids: PostResponse[] = [];
|
||||
for (const message of postDetails) {
|
||||
if (
|
||||
(message?.media?.length || 0) === 1
|
||||
) {
|
||||
const [{ message_id }] = await telegramBot.sendMediaGroup(
|
||||
accessToken,
|
||||
message?.media?.map((m) => ({
|
||||
type: m.url.indexOf('mp4') > -1 ? 'video' : 'photo',
|
||||
caption: message.message,
|
||||
media: m.url,
|
||||
})) || []
|
||||
);
|
||||
|
||||
ids.push({
|
||||
id: message.id,
|
||||
postId: String(message_id),
|
||||
releaseURL: `https://t.me/${id}/${message_id}`,
|
||||
status: 'completed',
|
||||
});
|
||||
} else {
|
||||
const { message_id } = await telegramBot.sendMessage(
|
||||
accessToken,
|
||||
message.message
|
||||
);
|
||||
|
||||
ids.push({
|
||||
id: message.id,
|
||||
postId: String(message_id),
|
||||
releaseURL: `https://t.me/${id}/${message_id}`,
|
||||
status: 'completed',
|
||||
});
|
||||
|
||||
if ((message?.media?.length || 0) > 0) {
|
||||
await telegramBot.sendMediaGroup(
|
||||
accessToken,
|
||||
message?.media?.map((m) => ({
|
||||
type: m.url.indexOf('mp4') > -1 ? 'video' : 'photo',
|
||||
media: m.url,
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,10 @@ export class EmailService {
|
|||
}
|
||||
|
||||
async sendEmail(to: string, subject: string, html: string, replyTo?: string) {
|
||||
if (to.indexOf('@') === -1) {
|
||||
return ;
|
||||
}
|
||||
|
||||
if (!process.env.EMAIL_FROM_ADDRESS || !process.env.EMAIL_FROM_NAME) {
|
||||
console.log(
|
||||
'Email sender information not found in environment variables'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
export class NewsletterService {
|
||||
static async register(email: string) {
|
||||
if (!process.env.BEEHIIVE_API_KEY || !process.env.BEEHIIVE_PUBLICATION_ID || process.env.NODE_ENV === 'development') {
|
||||
if (
|
||||
!process.env.BEEHIIVE_API_KEY ||
|
||||
!process.env.BEEHIIVE_PUBLICATION_ID ||
|
||||
process.env.NODE_ENV === 'development' ||
|
||||
email.indexOf('@') === -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const body = {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ interface VariableContextInterface {
|
|||
discordUrl: string;
|
||||
uploadDirectory: string;
|
||||
facebookPixel: string;
|
||||
telegramBotName: string;
|
||||
neynarClientId: string;
|
||||
isSecured: boolean;
|
||||
tolt: string;
|
||||
}
|
||||
const VariableContext = createContext({
|
||||
|
|
@ -23,7 +26,10 @@ const VariableContext = createContext({
|
|||
backendUrl: '',
|
||||
discordUrl: '',
|
||||
uploadDirectory: '',
|
||||
isSecured: false,
|
||||
telegramBotName: '',
|
||||
facebookPixel: '',
|
||||
neynarClientId: '',
|
||||
tolt: '',
|
||||
} as VariableContextInterface);
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
|
|
@ -37,9 +37,9 @@
|
|||
"@aws-sdk/client-s3": "3.712.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.712.0",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@copilotkit/react-core": "^1.5.11",
|
||||
"@copilotkit/react-textarea": "^1.5.11",
|
||||
"@copilotkit/react-ui": "^1.5.11",
|
||||
"@copilotkit/react-core": "^1.5.13",
|
||||
"@copilotkit/react-textarea": "^1.5.13",
|
||||
"@copilotkit/react-ui": "^1.5.13",
|
||||
"@copilotkit/runtime": "^1.4.7",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@langchain/community": "^0.3.19",
|
||||
|
|
@ -133,7 +133,9 @@
|
|||
"nestjs-real-ip": "^3.0.1",
|
||||
"next": "^14.2.14",
|
||||
"next-plausible": "^3.12.0",
|
||||
"node-telegram-bot-api": "^0.66.0",
|
||||
"nodemailer": "^6.9.15",
|
||||
"nostr-tools": "^2.10.4",
|
||||
"nx": "19.7.2",
|
||||
"openai": "^4.47.1",
|
||||
"polotno": "^2.10.5",
|
||||
|
|
@ -170,6 +172,7 @@
|
|||
"utf-8-validate": "^5.0.10",
|
||||
"uuid": "^10.0.0",
|
||||
"viem": "^2.22.9",
|
||||
"ws": "^8.18.0",
|
||||
"yargs": "^17.7.2",
|
||||
"yup": "^1.4.0",
|
||||
"zod": "^3.24.1"
|
||||
|
|
@ -187,6 +190,7 @@
|
|||
"@types/cookie-parser": "^1.4.6",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "18.16.9",
|
||||
"@types/node-telegram-bot-api": "^0.64.7",
|
||||
"@types/react": "18.3.1",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/uuid": "^9.0.8",
|
||||
|
|
|
|||
Loading…
Reference in New Issue