Skip to content

Commit

Permalink
feat: Add skeleton for SSO, add google as testing provider
Browse files Browse the repository at this point in the history
  • Loading branch information
aXenDeveloper committed Nov 24, 2024
1 parent 27e0c7b commit b0280fb
Show file tree
Hide file tree
Showing 25 changed files with 338 additions and 86 deletions.
1 change: 1 addition & 0 deletions apps/frontend/src/plugins/core/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@
"sign_in": {
"title": "Login",
"desc": "You don't have an account? <link></link>.",
"or": "or",
"sign_up": "Sign Up",
"email": "Email",
"password": "Password",
Expand Down
3 changes: 2 additions & 1 deletion packages/backend/src/core/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { HelperSignUpAuthService } from './services/sign_up/helper.service';
import { SendConfirmEmailAuthService } from './services/sign_up/send.confirm_email.service';
import { SignUpAuthService } from './services/sign_up/sign_up.service';
import { SettingsAuthModule } from './settings/settings.module';
import { SSOAuthModule } from './sso/sso.module';

@Module({
providers: [
Expand All @@ -25,6 +26,6 @@ import { SettingsAuthModule } from './settings/settings.module';
SignOutAuthService,
],
controllers: [AuthController],
imports: [SettingsAuthModule],
imports: [SettingsAuthModule, SSOAuthModule],
})
export class AuthModule {}
2 changes: 1 addition & 1 deletion packages/backend/src/core/auth/services/sign_in.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ export class SignInAuthService {
avatar_color: true,
},
});
if (!user) {
if (!user?.password) {
throw new HttpException('ACCESS_DENIED', HttpStatus.UNAUTHORIZED);
}

Expand Down
66 changes: 66 additions & 0 deletions packages/backend/src/core/auth/sso/sso.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { SSOAuthHelper } from '@/helpers/auth/sso.service';
import {
Controller,
ForbiddenException,
Get,
Param,
Query,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ApiTags } from '@nestjs/swagger';
import { SSOUrlAuthObj } from 'vitnode-shared/auth/sso.dto';

@ApiTags('Core')
@Controller('core/auth/sso')
export class SSOAuthController {
constructor(
private readonly configService: ConfigService,
private readonly ssoHelper: SSOAuthHelper,
) {}

@Get(':provider/callback')
async callbackSSO(
@Param('provider') provider: string,
@Query() query: Record<string, string>,
) {
const frontendUrl: string = this.configService.getOrThrow('frontend_url');

const body = {
client_id: '',
client_secret: '',
code: query.code,
grant_type: 'authorization_code',
redirect_uri: `${frontendUrl}/login/sso/google/callback`,
};

const res = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: JSON.stringify(body),
});

const tokenData: {
access_token?: string;
} = await res.json();
if (!tokenData.access_token) {
throw new ForbiddenException('Invalid token');
}

const userInfoResponse = await fetch(
'https://www.googleapis.com/oauth2/v2/userinfo',
{
headers: {
Authorization: `Bearer ${tokenData.access_token}`,
},
},
);

const userInfo = await userInfoResponse.json();

Check warning on line 57 in packages/backend/src/core/auth/sso/sso.controller.ts

View workflow job for this annotation

GitHub Actions / linting

'userInfo' is assigned a value but never used

return 'callback';
}

@Get(':provider')
getUrlSSO(@Param('provider') provider: string): SSOUrlAuthObj {
return this.ssoHelper.getSSO(provider).getUrl();
}
}
7 changes: 7 additions & 0 deletions packages/backend/src/core/auth/sso/sso.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { SSOAuthController } from './sso.controller';

Check warning on line 2 in packages/backend/src/core/auth/sso/sso.module.ts

View workflow job for this annotation

GitHub Actions / linting

Missed spacing between "@nestjs/common" and "./sso.controller" imports

@Module({
controllers: [SSOAuthController],
})
export class SSOAuthModule {}
9 changes: 9 additions & 0 deletions packages/backend/src/core/middleware/services/show.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import { ManifestWithLang } from 'vitnode-shared/manifest.dto';
import { ShowMiddlewareObj } from 'vitnode-shared/middleware.dto';

import { NavMiddlewareService } from './nav.service';
import { SSOAuthHelper } from '@/helpers/auth/sso.service';

Check warning on line 12 in packages/backend/src/core/middleware/services/show.service.ts

View workflow job for this annotation

GitHub Actions / linting

Expected "@/helpers/auth/sso.service" (external) to come before "./nav.service" (sibling)

@Injectable()
export class ShowMiddlewareService {
constructor(
private readonly databaseService: InternalDatabaseService,
private readonly mailService: EmailHelperService,
private readonly navService: NavMiddlewareService,
private readonly ssoHelper: SSOAuthHelper,
) {}

protected async getManifest({
Expand Down Expand Up @@ -78,6 +80,13 @@ export class ShowMiddlewareService {
force_login: config.settings.authorization.force_login,
lock_register: config.settings.authorization.lock_register,
},
auth_methods: {
password: true,
sso: this.ssoHelper.getSSOs().map(sso => ({
name: sso.name,
code: sso.code,
})),
},
plugins: ['admin', 'core', ...plugins.map(plugin => plugin.code)],
languages_code_default: langs.find(lang => lang.default)?.code ?? 'en',
is_email_enabled: this.mailService.checkIfEnable(),
Expand Down
23 changes: 21 additions & 2 deletions packages/backend/src/database/schema/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const core_users = pgTable(
name_seo: t.varchar({ length: 255 }).notNull().unique(),
name: t.varchar({ length: 255 }).notNull().unique(),
email: t.varchar({ length: 255 }).notNull().unique(),
password: t.varchar().notNull(),
password: t.varchar(),
joined_at: t.timestamp().notNull().defaultNow(),
newsletter: t.boolean().notNull().default(false),
avatar_color: t.varchar({ length: 6 }).notNull(),
Expand Down Expand Up @@ -39,7 +39,7 @@ export const core_users = pgTable(
],
);

export const core_users_relations = relations(core_users, ({ one }) => ({
export const core_users_relations = relations(core_users, ({ one, many }) => ({
group: one(core_groups, {
fields: [core_users.group_id],
references: [core_groups.id],
Expand All @@ -56,8 +56,27 @@ export const core_users_relations = relations(core_users, ({ one }) => ({
fields: [core_users.id],
references: [core_users_confirm_emails.user_id],
}),
sso: many(core_users_sso_tokens),
}));

export const core_users_sso_tokens = pgTable(
'core_users_sso_tokens',
t => ({
id: t.serial().primaryKey(),
user_id: t
.integer()
.references(() => core_users.id, {
onDelete: 'cascade',
})
.notNull(),
provider: t.varchar({ length: 100 }).notNull(),
id_provider: t.varchar({ length: 255 }).notNull(),
created_at: t.timestamp().notNull().defaultNow(),
updated_at: t.timestamp().notNull().defaultNow(),
}),
t => [index('core_users_sso_tokens_user_id_idx').on(t.user_id)],
);

export const core_files_avatars = pgTable('core_files_avatars', t => ({
id: t.serial().primaryKey(),
dir_folder: t.varchar({ length: 255 }).notNull(),
Expand Down
51 changes: 51 additions & 0 deletions packages/backend/src/helpers/auth/sso.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { SSOUrlAuthObj } from 'vitnode-shared/auth/sso.dto';

export interface SSOAuthItem {
name: string;
enabled: boolean;

Check warning on line 7 in packages/backend/src/helpers/auth/sso.service.ts

View workflow job for this annotation

GitHub Actions / linting

Expected "enabled" to come before "name"
getUrl: () => SSOUrlAuthObj;
code: string;

Check warning on line 9 in packages/backend/src/helpers/auth/sso.service.ts

View workflow job for this annotation

GitHub Actions / linting

Expected "code" to come before "getUrl"
}

@Injectable()
export class SSOAuthHelper {
constructor(private readonly configService: ConfigService) {}

getSSO(code: string): SSOAuthItem {
const item = this.getSSOs().find(sso => sso.code === code);
if (!item) {
throw new NotFoundException(`SSO provider with ${code} code not found`);
}

return item;
}

getSSOs(): SSOAuthItem[] {
const frontendUrl: string = this.configService.getOrThrow('frontend_url');
const redirectUri = (code: string) =>
`${frontendUrl}/login/sso/${code}/callback`;

return [
{
name: 'Google',
code: 'google',
enabled: true,
getUrl: () => {
const params = new URLSearchParams({
client_id:
'1067408430287-igio7a4koou4i26n8vvmqo4eqtcp9gka.apps.googleusercontent.com',
redirect_uri: redirectUri('google'),
response_type: 'code',
scope: 'openid profile email',
});

return {
url: `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`,
};
},
},
];
}
}
3 changes: 3 additions & 0 deletions packages/backend/src/helpers/helpers.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { FilesHelperService } from './files/files-helper.service';
import { StringLanguageHelper } from './string_language/helpers.service';
import { UserHelper } from './user.service';
import { SSOAuthHelper } from './auth/sso.service';

Check warning on line 22 in packages/backend/src/helpers/helpers.module.ts

View workflow job for this annotation

GitHub Actions / linting

Expected "./auth/sso.service" to come before "./user.service"

@Global()
@Module({})
Expand Down Expand Up @@ -62,6 +63,7 @@ export class GlobalHelpersModule {
DeviceAuthService,
UserHelper,
FilesHelperService,
SSOAuthHelper,
],
exports: [
EmailHelperService,
Expand All @@ -73,6 +75,7 @@ export class GlobalHelpersModule {
DeviceAuthService,
UserHelper,
FilesHelperService,
SSOAuthHelper,
],
};
}
Expand Down
1 change: 0 additions & 1 deletion packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@
"cropperjs": "^1.6.2",
"embla-carousel-react": "^8.5.1",
"emoji-mart": "^5.6.0",
"framer-motion": "^11.11.17",
"html-react-parser": "^5.1.18",
"lodash": "^4.17.21",
"lowlight": "^3.1.0",
Expand Down
68 changes: 31 additions & 37 deletions packages/frontend/src/components/ui/tag-input.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client';

import { AnimatePresence, motion } from 'framer-motion';
import { X } from 'lucide-react';
import React from 'react';

Expand Down Expand Up @@ -47,48 +46,43 @@ export const TagInput = ({
<div className="space-y-3">
{values.length > 0 && (
<div className="flex flex-wrap items-center gap-2">
<AnimatePresence>
{values.map(item => {
const onRemove = () => {
if (multiple) {
onChange(values.filter(value => value.id !== item.id));
{values.map(item => {
const onRemove = () => {
if (multiple) {
onChange(values.filter(value => value.id !== item.id));

return;
}
return;
}

onChange();
};
onChange();
};

return (
<motion.div
animate={{ opacity: 1, scale: 1 }}
className={badgeVariants({
variant: 'outline',
className: 'shrink-0 cursor-pointer [&>svg]:size-4',
})}
exit={{ opacity: 0, scale: 0.5 }}
initial={{ opacity: 0, scale: 0.5 }}
key={item.id}
layout
onClick={e => {
return (
<div
className={badgeVariants({
variant: 'outline',
className: 'shrink-0 cursor-pointer [&>svg]:size-4',
})}
key={item.id}
onClick={e => {
e.stopPropagation();
e.preventDefault();
onRemove();
}}
onKeyDown={e => {
if (e.key === 'Enter') {
e.stopPropagation();
e.preventDefault();
onRemove();
}}
onKeyDown={e => {
if (e.key === 'Enter') {
e.stopPropagation();
e.preventDefault();
onRemove();
}
}}
tabIndex={0}
>
{item.value} <X />
</motion.div>
);
})}
</AnimatePresence>
}
}}
role="button"
tabIndex={0}
>
{item.value} <X />
</div>
);
})}
</div>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const ContentCreateUserUsersMembersAdmin = () => {
const value: string = props.field.value ?? '';

return (
<div className="space-y-1">
<>
<AutoFormInput {...props} />
{props.field.value.length > 0 && (
<span className="text-muted-foreground mt-2 block max-w-md truncate text-sm">
Expand All @@ -35,7 +35,7 @@ export const ContentCreateUserUsersMembersAdmin = () => {
})}
</span>
)}
</div>
</>
);
},
},
Expand Down Expand Up @@ -63,7 +63,7 @@ export const ContentCreateUserUsersMembersAdmin = () => {
}, 0);

return (
<div className="space-y-1">
<>
<AutoFormInput {...props} type="password" />
{value.length > 0 && (
<div>
Expand All @@ -76,7 +76,7 @@ export const ContentCreateUserUsersMembersAdmin = () => {
/>
</div>
)}
</div>
</>
);
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const UserFooterNavBarMobile = () => {
const { onSubmit } = useSignOutApi({});

return (
<div className="my-4 flex flex-col px-2">
<div className="mb-4 flex flex-col px-2">
{user?.is_admin && (
<>
<Separator className="my-1" />
Expand Down
Loading

0 comments on commit b0280fb

Please sign in to comment.