Skip to content
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: 2 additions & 1 deletion AUTHORS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
## Developers

Riley Smith 2025-2026
Sophia Kist 2024-2025
Sophia Kist 2024-2025
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ The Submitty Extension for VS Code integrates the Submitty grading system direct

## Requirements

- A valid Submitty account.
- A valid Submitty account.
9 changes: 9 additions & 0 deletions src/interfaces/Responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,12 @@ export type LoginResponse = ApiResponse<{
export type GradableResponse = ApiResponse<{
[key: string]: Gradable;
}>;

/** Current user from `GET /api/me` (`data` field of the envelope). */
export interface User {
user_id: string;
user_given_name: string;
user_family_name: string;
}

export type UserResponse = ApiResponse<User>;
61 changes: 53 additions & 8 deletions src/services/apiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
CourseResponse,
LoginResponse,
GradableResponse,
UserResponse,
User,
} from '../interfaces/Responses';
import { AutoGraderDetails } from '../interfaces/AutoGraderDetails';

Expand All @@ -28,6 +30,7 @@ function getErrorMessage(error: unknown, fallback: string): string {
export class ApiService {
private client: ApiClient;
private static instance: ApiService;
private currentUser: User | null = null;

constructor(
private context: vscode.ExtensionContext,
Expand All @@ -42,6 +45,28 @@ export class ApiService {
*/
setAuthorizationToken(token: string): void {
this.client.setToken(token);
if (!token) {
this.currentUser = null;
}
}

getCurrentUser(): User | null {
return this.currentUser;
}

getCurrentUserId(): string | undefined {
return this.currentUser?.user_id;
}

/**
* Returns `user_id` from cache, or refetches `/api/me` if needed.
*/
async ensureCurrentUserId(): Promise<string> {
if (this.currentUser?.user_id) {
return this.currentUser.user_id;
}
const user = await this.fetchMe();
return user.user_id;
}

/**
Expand Down Expand Up @@ -81,13 +106,22 @@ export class ApiService {
}

/**
* Fetches the current authenticated user's profile from the API.
* @returns The current user data
* Fetches the current authenticated user's profile from the API and updates the cache.
* @returns The current user (`data` object from the API envelope)
*/
async fetchMe(): Promise<any> {
async fetchMe(): Promise<User> {
try {
const response = await this.client.get<any>('/api/me');
return response.data;
const response = await this.client.get<UserResponse>('/api/me');
const body = response.data;
if (
body?.status !== 'success' ||
typeof body.data?.user_id !== 'string' ||
!body.data.user_id
) {
throw new Error('Invalid response from /api/me.');
}
this.currentUser = body.data;
return body.data;
} catch (error: unknown) {
throw new Error(getErrorMessage(error, 'Failed to fetch me.'), {
cause: error,
Expand Down Expand Up @@ -205,6 +239,7 @@ export class ApiService {

/**
* Submits a VCS (version control) gradable to trigger autograding.
* Uses `user_id` from `/api/me` (see {@link fetchMe} / {@link ensureCurrentUserId}).
* @param term - The term (e.g. "s24")
* @param courseId - The course ID
* @param gradeableId - The gradeable/assignment ID
Expand All @@ -214,11 +249,21 @@ export class ApiService {
term: string,
courseId: string,
gradeableId: string
): Promise<any> {
): Promise<unknown> {
try {
const userId = await this.ensureCurrentUserId();
// git_repo_id is literally not used, but is required by the API *ugh*
const url = `/api/${term}/${courseId}/gradeable/${gradeableId}/upload?vcs_upload=true&git_repo_id=true`;
const response = await this.client.post<any>(url);
const url = `/api/${term}/${courseId}/gradeable/${gradeableId}/grade`;

const data = {
git_repo_id: true,
vcs_checkout: true,
user_id: userId,
};

const response = await this.client.post<unknown>(url, data, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
} catch (error: unknown) {
console.error('Error submitting VCS gradable:', error);
Expand Down
11 changes: 11 additions & 0 deletions src/services/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ export class AuthService {
this.apiService.setBaseUrl(baseUrl);
}

try {
await this.apiService.fetchMe();
} catch (error: unknown) {
const err = error instanceof Error ? error.message : String(error);
console.warn('Failed to load user profile (/api/me):', error);
vscode.window.showWarningMessage(
`Submitty: could not load profile (${err}). Some features may not work until you reload the window.`
);
}

return;
}

Expand Down Expand Up @@ -140,6 +150,7 @@ export class AuthService {
try {
// Perform login
await this.login(userId.trim(), password);
await this.apiService.fetchMe();

vscode.window.showInformationMessage(
'Successfully logged in to Submitty'
Expand Down
55 changes: 55 additions & 0 deletions src/sidebar/classes.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,29 @@
.homework-item:last-child {
border-bottom: none;
}
.profile-bar {
display: flex;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--vscode-widget-border);
}
.profile-button {
max-width: 100%;
padding: 6px 12px;
font-size: 13px;
cursor: default;
text-align: left;
background-color: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid
var(--vscode-button-border, var(--vscode-contrastBorder, transparent));
border-radius: 3px;
}
</style>
</head>
<body>
<div id="profileBar" class="profile-bar" hidden></div>
<section>
<h3>Courses</h3>
<div id="unarchivedCourses"></div>
Expand Down Expand Up @@ -94,9 +114,44 @@ <h3>Courses</h3>
});
}

function renderProfile(profile) {
const bar = document.getElementById('profileBar');
if (!bar) {
return;
}
if (
!profile ||
typeof profile !== 'object' ||
typeof profile.user_id !== 'string' ||
!profile.user_id
) {
bar.hidden = true;
bar.innerHTML = '';
return;
}
var gn =
typeof profile.user_given_name === 'string'
? profile.user_given_name
: '';
var fn =
typeof profile.user_family_name === 'string'
? profile.user_family_name
: '';
var display = (gn + ' ' + fn).trim() || profile.user_id;
var title = '@' + profile.user_id;
bar.hidden = false;
bar.innerHTML =
'<button type="button" class="profile-button" title="' +
escapeHtml(title) +
'">' +
escapeHtml(display) +
'</button>';
}

window.addEventListener('message', event => {
const { command, data } = event.data;
if (command === 'displayCourses') {
renderProfile(data && data.profile);
const courses = normalizeCourses(data || {});
const container = document.getElementById('unarchivedCourses');
if (courses.length === 0) {
Expand Down
20 changes: 18 additions & 2 deletions src/sidebarProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,19 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
})
);

let profile = this.apiService.getCurrentUser();
if (!profile) {
try {
await this.apiService.fetchMe();
profile = this.apiService.getCurrentUser();
} catch (error: unknown) {
console.warn('Could not load profile for sidebar:', error);
}
}

view.webview.postMessage({
command: MessageCommand.DISPLAY_COURSES,
data: { courses: coursesWithGradables },
data: { courses: coursesWithGradables, profile },
});
} catch (error: unknown) {
const err = error instanceof Error ? error.message : String(error);
Expand Down Expand Up @@ -247,7 +257,13 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
}
}

await this.apiService.submitVCSGradable(term, courseId, gradeableId);
console.log('Submitting VCS gradable...');
const response = await this.apiService.submitVCSGradable(
term,
courseId,
gradeableId
);
console.log('Response:', response);

const gradeDetails = await vscode.window.withProgress(
{
Expand Down
Loading