Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/verify #275

Merged
merged 7 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Alla märkbara ändringar ska dokumenteras i denna fil.
Baserat på [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
och följer [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.13.0]
- Verify user logic in user api, resolver and graph.

## [1.12.1] - 2024-09-20

### Tillagt
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ekorre-ts",
"version": "1.12.1",
"version": "1.13.0",
"description": "E-Sektionens backend",
"main": "src/index.ts",
"scripts": {
Expand Down
10 changes: 10 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ model PrismaUser {
website String?
luCard String? @unique
dateJoined DateTime @default(now()) @map("date_joined")
verifyInfo PrismaVerifyInfo? @relation(name: "PrismaVerifyInfoToPrismaUser")

@@index([firstName, lastName])
@@map("users")
Expand Down Expand Up @@ -328,6 +329,15 @@ model PrismaLoginProvider {
@@map("login_providers")
}

model PrismaVerifyInfo {
id Int @id @default(autoincrement())
user PrismaUser @relation(name: "PrismaVerifyInfoToPrismaUser", fields: [refUser], references: [username])
refUser String @unique @map("ref_user")
verifiedUntil DateTime @default(now()) @map("verified_until")

@@map("verify_info")
}

enum PrismaResourceType {
door
feature
Expand Down
43 changes: 40 additions & 3 deletions src/api/user.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { devGuard } from '@/util';
import { LoginProvider } from '@esek/auth-server';
import type { NewUser } from '@generated/graphql';
import { Prisma, PrismaLoginProvider, PrismaPasswordReset, PrismaUser } from '@prisma/client';
import { verify } from '@service/verify';
import crypto, { randomUUID } from 'crypto';

import {
Expand Down Expand Up @@ -40,7 +41,7 @@ export class UserAPI {
* @param hash The stored hash
* @param salt The stored salt
*/
private verifyUser(input: string, hash: string, salt: string): boolean {
private verifyPassword(input: string, hash: string, salt: string): boolean {
const equal = hash === this.hashPassword(input, salt);
return equal;
}
Expand Down Expand Up @@ -240,7 +241,7 @@ export class UserAPI {
throw new NotFoundError('Användaren finns inte');
}

if (!this.verifyUser(password, user.passwordHash, user.passwordSalt)) {
if (!this.verifyPassword(password, user.passwordHash, user.passwordSalt)) {
throw new UnauthenticatedError('Inloggningen misslyckades');
}

Expand Down Expand Up @@ -269,7 +270,7 @@ export class UserAPI {
throw new NotFoundError('Användaren finns inte');
}

if (!this.verifyUser(oldPassword, user.passwordHash, user.passwordSalt)) {
if (!this.verifyPassword(oldPassword, user.passwordHash, user.passwordSalt)) {
throw new UnauthenticatedError('Ditt gamla lösenord är fel');
}

Expand Down Expand Up @@ -654,4 +655,40 @@ export class UserAPI {

return providers;
}

async isUserVerified(username: string): Promise<boolean> {
const res = await prisma.prismaVerifyInfo.findUnique({ where: { refUser: username } });

//If never verified
if (!res) {
return false;
}

if (res.verifiedUntil.getTime() < Date.now()) {
return false;
}

return true;
}

async verifyUser(username: string, ssn: string): Promise<boolean> {
const userVerified = await this.isUserVerified(username);

const success = await verify(ssn, userVerified);

if (!success) {
throw new ServerError('Kudne inte verifiera användaren');
}

//13th of july in the coming year maybe good yes?
const verifiedUntil = new Date(new Date().getFullYear() + 1, 6, 13).toISOString();

const res = await prisma.prismaVerifyInfo.upsert({
where: { refUser: username },
create: { refUser: username, verifiedUntil: verifiedUntil },
update: { verifiedUntil: verifiedUntil },
});

return res != null;
}
}
5 changes: 5 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ const LATEXIFY = {
URL: process.env.LATEXIFY_URL ?? '',
};

const VERIFY = {
URL: process.env.VERIFY_URL ?? '',
};

const JWT = {
SECRET: (process.env.JWT_SECRET as string) ?? '',
};
Expand All @@ -86,6 +90,7 @@ const config = {
WIKI,
PDF_TO_PNG,
LATEXIFY,
VERIFY,
JWT,
};

Expand Down
10 changes: 10 additions & 0 deletions src/models/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ export type Mutation = {
updateUser: User;
validatePasswordResetToken: Scalars['Boolean'];
validateToken: Scalars['Boolean'];
verifyUser: Scalars['Boolean'];
};


Expand Down Expand Up @@ -749,6 +750,12 @@ export type MutationValidateTokenArgs = {
token: Scalars['String'];
};


export type MutationVerifyUserArgs = {
ssn: Scalars['String'];
username: Scalars['String'];
};

export type NewActivity = {
description?: InputMaybe<Scalars['String']>;
endDate?: InputMaybe<Scalars['DateTime']>;
Expand Down Expand Up @@ -1392,6 +1399,7 @@ export type User = {
/** Currents posts held by this user */
posts: Array<Post>;
username: Scalars['String'];
verified: Scalars['Boolean'];
website?: Maybe<Scalars['String']>;
wikiEdits: Scalars['Int'];
zipCode?: Maybe<Scalars['String']>;
Expand Down Expand Up @@ -1858,6 +1866,7 @@ export type MutationResolvers<ContextType = Context, ParentType extends Resolver
updateUser?: Resolver<ResolversTypes['User'], ParentType, ContextType, RequireFields<MutationUpdateUserArgs, 'input'>>;
validatePasswordResetToken?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationValidatePasswordResetTokenArgs, 'token' | 'username'>>;
validateToken?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationValidateTokenArgs, 'token'>>;
verifyUser?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationVerifyUserArgs, 'ssn' | 'username'>>;
}>;

export type NominationResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Nomination'] = ResolversParentTypes['Nomination']> = ResolversObject<{
Expand Down Expand Up @@ -1994,6 +2003,7 @@ export type UserResolvers<ContextType = Context, ParentType extends ResolversPar
postHistory?: Resolver<Array<ResolversTypes['UserPostHistoryEntry']>, ParentType, ContextType, Partial<UserPostHistoryArgs>>;
posts?: Resolver<Array<ResolversTypes['Post']>, ParentType, ContextType>;
username?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
verified?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
website?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
wikiEdits?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
zipCode?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
Expand Down
1 change: 1 addition & 0 deletions src/reducers/user.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function userReduce(user: PrismaUser): User {
wikiEdits: 0,
emergencyContacts: [],
loginProviders: [],
verified: false,
};
return u;
}
21 changes: 21 additions & 0 deletions src/resolvers/user.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Logger } from '@/logger';
import { Context } from '@/models/context';
import { reduce } from '@/reducers';
import { hasAccess, hasAuthenticated, stripObject } from '@/util';
import { PostAPI } from '@api/post';
import { UserAPI } from '@api/user';
import { userApi } from '@dataloader/user';
import { Feature, Resolvers, User } from '@generated/graphql';
Expand All @@ -13,6 +14,7 @@ import EWiki from '@service/wiki';

const api = new UserAPI();
const wiki = new EWiki();
const postApi = new PostAPI();

const logger = Logger.getLogger('UserResolver');

Expand Down Expand Up @@ -112,6 +114,21 @@ const userResolver: Resolvers = {

return providers;
},
verified: async ({ username }, _, ctx) => {
await hasAuthenticated(ctx);

if (!username) {
return false;
}

const [verified, posts] = await Promise.all([
api.isUserVerified(username),
postApi.getPostsForUser(username, false),
]);

//Treated as verified if you have a post
return verified || posts.length > 0;
},
},
Query: {
me: async (_, __, { getUsername }) => {
Expand Down Expand Up @@ -234,6 +251,10 @@ const userResolver: Resolvers = {
const res = await api.getSingleUser(username);
return userReduce(res);
},
verifyUser: async (_, { username, ssn }) => {
const success = await api.verifyUser(username, ssn);
return success;
},
},
};

Expand Down
2 changes: 2 additions & 0 deletions src/schemas/user.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Mutation {
resetPassword(username: String!, token: String!, password: String!): Boolean!
changePassword(oldPassword: String!, newPassword: String!): Boolean!
casCreateUser(input: NewUser!, hash: String!): User!
verifyUser(username: String!, ssn: String!): Boolean!
}

type User {
Expand Down Expand Up @@ -54,6 +55,7 @@ type User {
postHistory(current: Boolean): [UserPostHistoryEntry!]!
wikiEdits: Int!
loginProviders: [LoginProvider]!
verified: Boolean!
}

input NewUser {
Expand Down
18 changes: 18 additions & 0 deletions src/services/verify.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import config from '@/config';
import axios from 'axios';

const {
VERIFY: { URL },
} = config;

export const verify = async (ssn: string, userVerified: boolean) => {
const options = {
method: 'POST',
url: URL,
headers: { 'content-type': 'application/json' },
data: { ssn: ssn.length == 10 ? ssn : ssn.slice(2), alreadyVerified: userVerified }, //Case for both 10 and 12 digit ssn.
};
const data = await axios.request(options);

return data.status === 200;
};
27 changes: 27 additions & 0 deletions test/unit/user.api.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import prisma from '@/api/prisma';
import { UserAPI } from '@/api/user.api';
import { BadRequestError, NotFoundError, UnauthenticatedError } from '@/errors/request.errors';
import type { LoginProvider as LoginProviderType } from '@esek/auth-server';
Expand Down Expand Up @@ -359,6 +360,32 @@ test('getting number of members', async () => {
expect(numberOfMembers).toBeGreaterThanOrEqual(3);
});

test('Check if user verified', async () => {
await expect(api.isUserVerified(mockNewUser0.username)).resolves.toBeFalsy();

//this is ugly sorry
await prisma.prismaVerifyInfo.create({
data: {
refUser: mockNewUser0.username,
verifiedUntil: new Date(new Date().getFullYear() + 1, 6, 13).toISOString(),
},
});

await expect(api.isUserVerified(mockNewUser0.username)).resolves.toBeTruthy();

//this is ugly sorry
await prisma.prismaVerifyInfo.update({
where: { refUser: mockNewUser0.username },
data: {
verifiedUntil: new Date(new Date().getFullYear() - 1, 6, 13).toISOString(),
},
});

await expect(api.isUserVerified(mockNewUser0.username)).resolves.toBeFalsy();

await prisma.prismaVerifyInfo.deleteMany();
});

describe('login providers', () => {
beforeAll(async () => {
await api.createUser(mockNewUser1);
Expand Down
1 change: 1 addition & 0 deletions test/unit/user.reducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ test('that password is reduced properly', () => {
emergencyContacts: [],
loginProviders: [],
luCard: null,
verified: false,
};

expect(userReduce(dummyDbUser)).toStrictEqual(compare);
Expand Down
Loading